SHA256
Добавить смену профилей, пока не работает
This commit is contained in:
+2
-1
@@ -1274,7 +1274,8 @@ function renderApp() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.session.isAuthorized && PRE_AUTH_PAGES.includes(pageId)) {
|
||||
const addingProfile = state.session.isAuthorized && String(state.authReturnHash || '').trim() === '/profiles';
|
||||
if (state.session.isAuthorized && PRE_AUTH_PAGES.includes(pageId) && !addingProfile) {
|
||||
navigate('messages-list');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { renderHeader } from '../components/header.js';
|
||||
import {
|
||||
authService,
|
||||
authorizeSession,
|
||||
consumeAuthReturnPage,
|
||||
isAddingProfileLogin,
|
||||
clearAuthMessages,
|
||||
clearBrowserClientData,
|
||||
refreshSessions,
|
||||
@@ -205,10 +207,13 @@ export function render({ navigate }) {
|
||||
try {
|
||||
await authService.reconnect(state.entrySettings.shineServer);
|
||||
const session = await authService.createSessionFromImportedSecrets(scannedTransfer.login, scannedTransfer.keys);
|
||||
await terminateCurrentSession({ closeServerSession: true });
|
||||
const addingProfile = isAddingProfileLogin();
|
||||
if (!addingProfile) {
|
||||
await terminateCurrentSession({ closeServerSession: true });
|
||||
clearBrowserClientData();
|
||||
await clearClientAuthData().catch(() => {});
|
||||
}
|
||||
await clearStoredMessages(session.login).catch(() => {});
|
||||
clearBrowserClientData();
|
||||
await clearClientAuthData().catch(() => {});
|
||||
await saveEncryptedUserSecrets(session.login, session.storagePwd, scannedTransfer.keys);
|
||||
await authService.persistSessionMaterial(session.login, session.sessionMaterial);
|
||||
const resumed = await authService.resumeSession(session.login, session.sessionId);
|
||||
@@ -221,7 +226,7 @@ export function render({ navigate }) {
|
||||
state.loginDraft.password = '';
|
||||
await refreshSessions();
|
||||
setAuthInfo(`Вход по QR-коду выполнен для @${resumed.login || session.login}.`);
|
||||
navigate('profile-view');
|
||||
navigate(consumeAuthReturnPage('profile-view'));
|
||||
} catch (error) {
|
||||
const message = toUserMessage(error, 'Не удалось войти по QR-коду.');
|
||||
setAuthError(message);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
authService,
|
||||
authorizeSession,
|
||||
consumeAuthReturnPage,
|
||||
isAddingProfileLogin,
|
||||
clearAuthMessages,
|
||||
clearBrowserClientData,
|
||||
refreshSessions,
|
||||
@@ -182,10 +184,13 @@ export function render({ navigate }) {
|
||||
|
||||
const finalizeAuthorizedLogin = async (keys, login) => {
|
||||
const session = await authService.createSessionFromImportedSecrets(login, keys);
|
||||
await terminateCurrentSession({ closeServerSession: true, notifySessionReset: false });
|
||||
const addingProfile = isAddingProfileLogin();
|
||||
if (!addingProfile) {
|
||||
await terminateCurrentSession({ closeServerSession: true, notifySessionReset: false });
|
||||
clearBrowserClientData();
|
||||
await clearClientAuthData().catch(() => {});
|
||||
}
|
||||
await clearStoredMessages(session.login).catch(() => {});
|
||||
clearBrowserClientData();
|
||||
await clearClientAuthData().catch(() => {});
|
||||
await saveEncryptedUserSecrets(session.login, session.storagePwd, keys);
|
||||
await authService.persistSessionMaterial(session.login, session.sessionMaterial);
|
||||
const resumed = await authService.resumeSession(session.login, session.sessionId);
|
||||
@@ -199,7 +204,7 @@ export function render({ navigate }) {
|
||||
await refreshSessions();
|
||||
setAuthInfo(`Вход через другое устройство выполнен для @${resumed.login || session.login}.`);
|
||||
showToast(`Устройство подключено для @${resumed.login || session.login}`);
|
||||
navigate('profile-view');
|
||||
navigate(consumeAuthReturnPage('profile-view'));
|
||||
};
|
||||
|
||||
const finalizeAuthorizedSessionAttach = async (payloadSession, login, requesterKeys) => {
|
||||
@@ -215,10 +220,13 @@ export function render({ navigate }) {
|
||||
sessionType: Number(payloadSession?.sessionType || 50) || 50,
|
||||
};
|
||||
|
||||
await terminateCurrentSession({ closeServerSession: true, notifySessionReset: false });
|
||||
const addingProfile = isAddingProfileLogin();
|
||||
if (!addingProfile) {
|
||||
await terminateCurrentSession({ closeServerSession: true, notifySessionReset: false });
|
||||
clearBrowserClientData();
|
||||
await clearClientAuthData().catch(() => {});
|
||||
}
|
||||
await clearStoredMessages(login).catch(() => {});
|
||||
clearBrowserClientData();
|
||||
await clearClientAuthData().catch(() => {});
|
||||
await authService.persistSessionMaterial(login, sessionMaterial);
|
||||
const resumed = await authService.resumeSession(login, sessionId);
|
||||
authorizeSession({
|
||||
@@ -231,7 +239,7 @@ export function render({ navigate }) {
|
||||
await refreshSessions();
|
||||
setAuthInfo(`Session-only вход выполнен для @${resumed.login || login}.`);
|
||||
showToast(`Wallet-session подключена для @${resumed.login || login}`);
|
||||
navigate('profile-view');
|
||||
navigate(consumeAuthReturnPage('profile-view'));
|
||||
};
|
||||
|
||||
const schedulePoll = () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
import {
|
||||
authService,
|
||||
cancelAddProfileLogin,
|
||||
clearAuthMessages,
|
||||
setAuthBusy,
|
||||
setAuthError,
|
||||
@@ -155,7 +156,17 @@ export function render({ navigate }) {
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: '',
|
||||
leftAction: { label: '←', onClick: () => navigate('start-view') },
|
||||
leftAction: {
|
||||
label: '←',
|
||||
onClick: async () => {
|
||||
if (state.authReturnHash === '/profiles') {
|
||||
await cancelAddProfileLogin();
|
||||
navigate('profiles-view');
|
||||
return;
|
||||
}
|
||||
navigate('start-view');
|
||||
},
|
||||
},
|
||||
}),
|
||||
panel,
|
||||
);
|
||||
|
||||
@@ -41,9 +41,19 @@ export function render({ navigate }) {
|
||||
addButton.type = 'button';
|
||||
addButton.className = 'secondary-btn';
|
||||
addButton.textContent = 'Добавить профиль';
|
||||
addButton.addEventListener('click', () => {
|
||||
prepareAddProfileLogin();
|
||||
navigate('login-view');
|
||||
addButton.addEventListener('click', async () => {
|
||||
addButton.disabled = true;
|
||||
status.hidden = false;
|
||||
status.className = 'status-line';
|
||||
status.textContent = 'Подготавливаем вход в новый профиль…';
|
||||
try {
|
||||
await prepareAddProfileLogin();
|
||||
navigate('login-view');
|
||||
} catch (error) {
|
||||
status.className = 'status-line is-unavailable';
|
||||
status.textContent = `Не удалось начать добавление профиля: ${error?.message || 'unknown'}`;
|
||||
addButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
const closeAllButton = document.createElement('button');
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
import {
|
||||
authService,
|
||||
cancelAddProfileLogin,
|
||||
consumeAuthReturnPage,
|
||||
authorizeSession,
|
||||
refreshSessions,
|
||||
resetRegistrationFlow,
|
||||
@@ -103,7 +105,12 @@ export function render({ navigate }) {
|
||||
cancelButton.className = 'ghost-btn';
|
||||
cancelButton.type = 'button';
|
||||
cancelButton.textContent = 'Отмена';
|
||||
cancelButton.addEventListener('click', () => {
|
||||
cancelButton.addEventListener('click', async () => {
|
||||
if (state.authReturnHash === '/profiles') {
|
||||
await cancelAddProfileLogin();
|
||||
navigate('profiles-view');
|
||||
return;
|
||||
}
|
||||
resetRegistrationFlow();
|
||||
navigate('start-view');
|
||||
});
|
||||
@@ -174,13 +181,7 @@ export function render({ navigate }) {
|
||||
setAuthInfo(isLoginFlow
|
||||
? `Ключи сохранены. Вы вошли как @${state.registrationDraft.login}. Далее откройте вкладку «Каналы».`
|
||||
: `Ключи сохранены. Регистрация завершена для @${state.registrationDraft.login}. Далее откройте вкладку «Каналы».`);
|
||||
const nextHash = String(state.authReturnHash || '').trim();
|
||||
state.authReturnHash = '';
|
||||
if (nextHash.startsWith('/')) {
|
||||
navigate(nextHash.slice(1));
|
||||
} else {
|
||||
navigate('profile-view');
|
||||
}
|
||||
navigate(consumeAuthReturnPage('profile-view'));
|
||||
} catch (error) {
|
||||
const message = toUserMessage(error, 'Не удалось сохранить ключи на устройстве.');
|
||||
setAuthError(message);
|
||||
@@ -196,7 +197,12 @@ export function render({ navigate }) {
|
||||
title: 'Сохранение ключей',
|
||||
leftAction: {
|
||||
label: '←',
|
||||
onClick: () => {
|
||||
onClick: async () => {
|
||||
if (state.authReturnHash === '/profiles') {
|
||||
await cancelAddProfileLogin();
|
||||
navigate('profiles-view');
|
||||
return;
|
||||
}
|
||||
resetRegistrationFlow();
|
||||
navigate('start-view');
|
||||
},
|
||||
|
||||
@@ -999,6 +999,8 @@ export class AuthService {
|
||||
constructor(serverUrl) {
|
||||
this.serverUrl = normalizeServerUrl(serverUrl);
|
||||
this.ws = new WsJsonClient(this.serverUrl);
|
||||
this.eventListeners = new Map();
|
||||
this.wsEventUnsubscribers = new Map();
|
||||
this.headerHashCache = new Map();
|
||||
this.writeLocks = new Map();
|
||||
this.passwordKeyBundleCache = new Map();
|
||||
@@ -1008,14 +1010,39 @@ export class AuthService {
|
||||
this.remoteAddBlockSessionId = '';
|
||||
}
|
||||
|
||||
async reconnect(serverUrl) {
|
||||
bindRegisteredEventsToCurrentWs() {
|
||||
this.wsEventUnsubscribers.forEach((unsubscribe) => {
|
||||
try { unsubscribe?.(); } catch {}
|
||||
});
|
||||
this.wsEventUnsubscribers.clear();
|
||||
|
||||
this.eventListeners.forEach((_handlers, op) => {
|
||||
const unsubscribe = this.ws.onEvent(op, (data) => {
|
||||
const handlers = this.eventListeners.get(op);
|
||||
if (!handlers) return;
|
||||
handlers.forEach((handler) => {
|
||||
try { handler(data); } catch {}
|
||||
});
|
||||
});
|
||||
this.wsEventUnsubscribers.set(op, unsubscribe);
|
||||
});
|
||||
}
|
||||
|
||||
resetConnection(serverUrl = this.serverUrl, { clearSessionContext = true } = {}) {
|
||||
const normalized = normalizeServerUrl(serverUrl);
|
||||
if (normalized === this.serverUrl) return;
|
||||
this.ws.close();
|
||||
try { this.ws?.close(); } catch {}
|
||||
this.serverUrl = normalized;
|
||||
this.ws = new WsJsonClient(this.serverUrl);
|
||||
this.headerHashCache = new Map();
|
||||
this.writeLocks.clear();
|
||||
this.bindRegisteredEventsToCurrentWs();
|
||||
if (clearSessionContext) this.clearActiveSessionContext();
|
||||
}
|
||||
|
||||
async reconnect(serverUrl) {
|
||||
const normalized = normalizeServerUrl(serverUrl);
|
||||
if (normalized === this.serverUrl) return;
|
||||
this.resetConnection(normalized, { clearSessionContext: false });
|
||||
}
|
||||
|
||||
setActiveSessionContext({ login = '', sessionId = '' } = {}) {
|
||||
@@ -2514,7 +2541,28 @@ export class AuthService {
|
||||
|
||||
|
||||
onEvent(op, handler) {
|
||||
return this.ws.onEvent(op, handler);
|
||||
if (!op || typeof handler !== 'function') return () => {};
|
||||
if (!this.eventListeners.has(op)) {
|
||||
this.eventListeners.set(op, new Set());
|
||||
const unsubscribe = this.ws.onEvent(op, (data) => {
|
||||
const handlers = this.eventListeners.get(op);
|
||||
if (!handlers) return;
|
||||
handlers.forEach((callback) => {
|
||||
try { callback(data); } catch {}
|
||||
});
|
||||
});
|
||||
this.wsEventUnsubscribers.set(op, unsubscribe);
|
||||
}
|
||||
const handlers = this.eventListeners.get(op);
|
||||
handlers.add(handler);
|
||||
return () => {
|
||||
handlers.delete(handler);
|
||||
if (handlers.size) return;
|
||||
this.eventListeners.delete(op);
|
||||
const unsubscribe = this.wsEventUnsubscribers.get(op);
|
||||
try { unsubscribe?.(); } catch {}
|
||||
this.wsEventUnsubscribers.delete(op);
|
||||
};
|
||||
}
|
||||
|
||||
async upsertPushToken({ endpoint, p256dhKey, authKey, sessionId, platform = 'web', userAgent = navigator.userAgent || '' }) {
|
||||
|
||||
+108
-13
@@ -1206,14 +1206,23 @@ export async function switchToSavedProfile(login) {
|
||||
if (!target) throw new Error('Профиль не найден на этом устройстве');
|
||||
if (targetLogin === normalizeProfileLogin(state.session.login)) return target;
|
||||
|
||||
const origin = {
|
||||
login: String(state.session.login || '').trim(),
|
||||
sessionId: String(state.session.sessionId || '').trim(),
|
||||
isLocalDemo: Boolean(state.session.isLocalDemo),
|
||||
server: String(state.entrySettings.shineServer || DEFAULT_SHINE_SERVER).trim(),
|
||||
};
|
||||
const targetServer = String(target?.entrySettings?.shineServer || state.entrySettings.shineServer || DEFAULT_SHINE_SERVER).trim();
|
||||
const probe = new AuthService(targetServer);
|
||||
|
||||
// В каждый момент времени держим только один WebSocket: сначала полностью
|
||||
// закрываем transport активного профиля, затем создаём новый для target.
|
||||
authService.resetConnection(targetServer, { clearSessionContext: true });
|
||||
try {
|
||||
await probe.reconnect(targetServer);
|
||||
const resumed = await probe.resumeSession(target.login, target.sessionId);
|
||||
const resumed = await authService.resumeSession(target.login, target.sessionId);
|
||||
target.login = resumed.login || target.login;
|
||||
target.sessionId = resumed.sessionId || target.sessionId;
|
||||
target.updatedAtMs = Date.now();
|
||||
authService.setActiveSessionContext({ login: target.login, sessionId: target.sessionId });
|
||||
persistProfileStoreRaw(loadProfileStoreRaw().map((item) => (
|
||||
normalizeProfileLogin(item.login) === targetLogin ? target : item
|
||||
)));
|
||||
@@ -1223,8 +1232,23 @@ export async function switchToSavedProfile(login) {
|
||||
persistEntrySettings({ ...state.entrySettings, ...target.entrySettings });
|
||||
}
|
||||
return target;
|
||||
} finally {
|
||||
probe.close();
|
||||
} catch (switchError) {
|
||||
// Если новый профиль не поднялся — создаём новый socket обратно для старого.
|
||||
try {
|
||||
authService.resetConnection(origin.server, { clearSessionContext: true });
|
||||
if (origin.login && origin.sessionId && !origin.isLocalDemo) {
|
||||
const restored = await authService.resumeSession(origin.login, origin.sessionId);
|
||||
authService.setActiveSessionContext({
|
||||
login: restored?.login || origin.login,
|
||||
sessionId: restored?.sessionId || origin.sessionId,
|
||||
});
|
||||
} else if (origin.login) {
|
||||
authService.setActiveSessionContext({ login: origin.login, sessionId: origin.sessionId });
|
||||
}
|
||||
} catch (restoreError) {
|
||||
console.warn('[profiles] failed to restore previous profile connection after switch failure', restoreError);
|
||||
}
|
||||
throw switchError;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1233,20 +1257,40 @@ async function closeSavedProfileSessionBestEffort(profile) {
|
||||
const cleanSessionId = String(profile.sessionId || '').trim();
|
||||
if (!cleanSessionId) return;
|
||||
const normalized = normalizeProfileLogin(profile.login);
|
||||
if (normalized === normalizeProfileLogin(state.session.login) && state.session.isAuthorized) {
|
||||
const activeNormalized = normalizeProfileLogin(state.session.login);
|
||||
if (normalized === activeNormalized && state.session.isAuthorized) {
|
||||
try { await authService.closeSession(cleanSessionId); } catch {}
|
||||
return;
|
||||
}
|
||||
|
||||
const origin = {
|
||||
login: String(state.session.login || '').trim(),
|
||||
sessionId: String(state.session.sessionId || '').trim(),
|
||||
isLocalDemo: Boolean(state.session.isLocalDemo),
|
||||
server: String(state.entrySettings.shineServer || DEFAULT_SHINE_SERVER).trim(),
|
||||
};
|
||||
const targetServer = String(profile?.entrySettings?.shineServer || state.entrySettings.shineServer || DEFAULT_SHINE_SERVER).trim();
|
||||
const probe = new AuthService(targetServer);
|
||||
try {
|
||||
await probe.reconnect(targetServer);
|
||||
await probe.resumeSession(profile.login, cleanSessionId);
|
||||
await probe.closeSession(cleanSessionId);
|
||||
authService.resetConnection(targetServer, { clearSessionContext: true });
|
||||
await authService.resumeSession(profile.login, cleanSessionId);
|
||||
await authService.closeSession(cleanSessionId);
|
||||
} catch {
|
||||
// Закрытие профиля на устройстве не блокируем из-за недоступного сервера.
|
||||
} finally {
|
||||
probe.close();
|
||||
try {
|
||||
authService.resetConnection(origin.server, { clearSessionContext: true });
|
||||
if (origin.login && origin.sessionId && !origin.isLocalDemo) {
|
||||
const restored = await authService.resumeSession(origin.login, origin.sessionId);
|
||||
authService.setActiveSessionContext({
|
||||
login: restored?.login || origin.login,
|
||||
sessionId: restored?.sessionId || origin.sessionId,
|
||||
});
|
||||
} else if (origin.login) {
|
||||
authService.setActiveSessionContext({ login: origin.login, sessionId: origin.sessionId });
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[profiles] failed to restore active profile after closing another profile', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1281,8 +1325,16 @@ export async function closeSavedProfile(login) {
|
||||
export async function closeAllSavedProfiles() {
|
||||
const items = loadProfileStoreRaw();
|
||||
for (const item of items) {
|
||||
await closeSavedProfileSessionBestEffort(item);
|
||||
await clearStoredMessages(item.login).catch(() => {});
|
||||
if (item.isLocalDemo || !String(item.sessionId || '').trim()) continue;
|
||||
const targetServer = String(item?.entrySettings?.shineServer || state.entrySettings.shineServer || DEFAULT_SHINE_SERVER).trim();
|
||||
try {
|
||||
authService.resetConnection(targetServer, { clearSessionContext: true });
|
||||
await authService.resumeSession(item.login, item.sessionId);
|
||||
await authService.closeSession(item.sessionId);
|
||||
} catch {
|
||||
// Все локальные профили всё равно закрываем, даже если один сервер недоступен.
|
||||
}
|
||||
}
|
||||
persistProfileStoreRaw([]);
|
||||
setActiveProfileLoginRaw('');
|
||||
@@ -1291,10 +1343,53 @@ export async function closeAllSavedProfiles() {
|
||||
authService.clearActiveSessionContext();
|
||||
}
|
||||
|
||||
export function prepareAddProfileLogin() {
|
||||
export function isAddingProfileLogin() {
|
||||
return state.session.isAuthorized && String(state.authReturnHash || '').trim() === '/profiles';
|
||||
}
|
||||
|
||||
export async function prepareAddProfileLogin() {
|
||||
state.loginDraft.login = '';
|
||||
state.loginDraft.password = '';
|
||||
clearAuthMessages();
|
||||
// While an existing profile stays authorized, PRE_AUTH login pages are normally
|
||||
// blocked by app.js. This return target also acts as an explicit add-profile mode.
|
||||
state.authReturnHash = '/profiles';
|
||||
|
||||
// Не пытаемся авторизовать второй login через уже authenticated socket.
|
||||
// Старую серверную сессию НЕ закрываем: закрываем только локальный transport.
|
||||
authService.resetConnection(state.entrySettings.shineServer, { clearSessionContext: true });
|
||||
}
|
||||
|
||||
export async function cancelAddProfileLogin() {
|
||||
const wasAddingProfile = isAddingProfileLogin();
|
||||
const shouldRestoreActiveConnection = wasAddingProfile
|
||||
&& state.session.isAuthorized
|
||||
&& Boolean(String(state.session.login || '').trim())
|
||||
&& Boolean(String(state.session.sessionId || '').trim());
|
||||
|
||||
state.authReturnHash = '';
|
||||
state.loginDraft.login = '';
|
||||
state.loginDraft.password = '';
|
||||
resetRegistrationFlow();
|
||||
clearAuthMessages();
|
||||
|
||||
if (shouldRestoreActiveConnection) {
|
||||
try {
|
||||
authService.resetConnection(state.entrySettings.shineServer, { clearSessionContext: true });
|
||||
await authService.resumeSession(state.session.login, state.session.sessionId);
|
||||
authService.setActiveSessionContext({ login: state.session.login, sessionId: state.session.sessionId });
|
||||
} catch (error) {
|
||||
console.warn('[profiles] failed to restore active profile connection after cancelling add-profile flow', error);
|
||||
}
|
||||
}
|
||||
return wasAddingProfile;
|
||||
}
|
||||
|
||||
export function consumeAuthReturnPage(fallback = 'profile-view') {
|
||||
const nextHash = String(state.authReturnHash || '').trim();
|
||||
state.authReturnHash = '';
|
||||
if (nextHash.startsWith('/')) return nextHash.slice(1) || fallback;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function refreshRegistrationBalance() {
|
||||
|
||||
Reference in New Issue
Block a user