Синхронизировать регистр логинов и доработать UI каналов

This commit is contained in:
AidarKC
2026-08-19 14:30:29 +04:00
parent e8a4713f6e
commit ce30b77328
29 changed files with 6729 additions and 815 deletions
+50 -19
View File
@@ -1022,6 +1022,18 @@ export class AuthService {
return response.payload || {};
}
async resolveCanonicalDisplayLogin(login) {
const cleanLogin = String(login || '').trim();
if (!cleanLogin) return '';
try {
const user = await this.getUser(cleanLogin);
const canonicalLogin = String(user?.login || '').trim();
return canonicalLogin || cleanLogin;
} catch {
return cleanLogin;
}
}
async ensureLoginFree(login) {
const payload = await this.getUser(login);
return payload.exists !== true;
@@ -1127,8 +1139,10 @@ export class AuthService {
const sessionId = createResp?.payload?.sessionId;
if (!sessionId) throw new Error('CreateAuthSession: не вернулся sessionId');
const canonicalLogin = await this.resolveCanonicalDisplayLogin(cleanLogin);
return {
login: cleanLogin,
login: canonicalLogin,
sessionId,
storagePwd,
sessionMaterial: {
@@ -1213,8 +1227,10 @@ export class AuthService {
const sessionId = createResp?.payload?.sessionId;
if (!sessionId) throw new Error('CreateAuthSession: не вернулся sessionId');
const canonicalLogin = await tempAuth.resolveCanonicalDisplayLogin(cleanLogin);
return {
login: cleanLogin,
login: canonicalLogin,
sessionId,
storagePwd,
sessionKey: cleanSessionKey,
@@ -1287,8 +1303,10 @@ export class AuthService {
const storagePwd = loginResp?.payload?.storagePwd;
if (!storagePwd) throw new Error('SessionLogin: не вернулся storagePwd');
const canonicalLogin = await this.resolveCanonicalDisplayLogin(cleanLogin);
return {
login: cleanLogin,
login: canonicalLogin,
sessionId: targetSessionId,
storagePwd,
};
@@ -2476,6 +2494,10 @@ export class AuthService {
return response.payload || {};
}
normalizeDmLogin(login) {
return String(login || '').trim().toLowerCase();
}
async buildSignedDmBlock({
signerLogin,
fromLogin,
@@ -2488,9 +2510,9 @@ export class AuthService {
reencryptedAtMs = 0,
bodyBytes = new Uint8Array(0),
}) {
const cleanSignerLogin = String(signerLogin || '').trim();
const cleanFromLogin = String(fromLogin || '').trim();
const cleanToLogin = String(toLogin || '').trim();
const cleanSignerLogin = this.normalizeDmLogin(signerLogin);
const cleanFromLogin = this.normalizeDmLogin(fromLogin);
const cleanToLogin = this.normalizeDmLogin(toLogin);
if (!cleanSignerLogin || !cleanFromLogin || !cleanToLogin) throw new Error('Не передан signerLogin/fromLogin/toLogin');
if (!storagePwd) throw new Error('Не передан storagePwd для подписи');
if (!(bodyBytes instanceof Uint8Array) || bodyBytes.length > DM_MAX_ENCRYPTED_BODY_BYTES) {
@@ -2590,8 +2612,8 @@ export class AuthService {
revisionTimeMs = 0,
reencryptedAtMs = 0,
}) {
const cleanFromLogin = String(login || '').trim();
const cleanToLogin = String(toLogin || '').trim();
const cleanFromLogin = this.normalizeDmLogin(login);
const cleanToLogin = this.normalizeDmLogin(toLogin);
const cleanText = String(text || '');
if (!cleanFromLogin || !cleanToLogin) throw new Error('Не передан login/toLogin');
const normalizedTimeMs = Number(timeMs);
@@ -2674,8 +2696,8 @@ export class AuthService {
}
async deleteDirectMessage({ login, toLogin, storagePwd, timeMs, nonce, revisionTimeMs, deleteByRecipient = false }) {
const cleanLogin = String(login || '').trim();
const cleanPeerLogin = String(toLogin || '').trim();
const cleanLogin = this.normalizeDmLogin(login);
const cleanPeerLogin = this.normalizeDmLogin(toLogin);
if (!cleanLogin || !cleanPeerLogin) throw new Error('Не передан login/toLogin');
const normalizedTimeMs = Number(timeMs);
const normalizedNonce = Number(nonce);
@@ -2706,14 +2728,23 @@ export class AuthService {
}
async sendReadReceipt({ login, toLogin, storagePwd, refToLogin, refFromLogin, refTimeMs, refNonce }) {
const cleanLogin = this.normalizeDmLogin(login);
const cleanToLogin = this.normalizeDmLogin(toLogin);
const cleanRefToLogin = this.normalizeDmLogin(refToLogin);
const cleanRefFromLogin = this.normalizeDmLogin(refFromLogin);
const timeMs = Date.now();
const nonce = Math.floor(Math.random() * 0x100000000);
const payload = buildReadReceiptPayloadBytes({ refToLogin, refFromLogin, refTimeMs, refNonce });
const payload = buildReadReceiptPayloadBytes({
refToLogin: cleanRefToLogin,
refFromLogin: cleanRefFromLogin,
refTimeMs,
refNonce,
});
const type3 = await this.buildSignedDmBlock({
signerLogin: login,
fromLogin: login,
toLogin,
signerLogin: cleanLogin,
fromLogin: cleanLogin,
toLogin: cleanToLogin,
storagePwd,
timeMs,
nonce,
@@ -2721,9 +2752,9 @@ export class AuthService {
bodyBytes: payload,
});
const type4 = await this.buildSignedDmBlock({
signerLogin: login,
fromLogin: login,
toLogin,
signerLogin: cleanLogin,
fromLogin: cleanLogin,
toLogin: cleanToLogin,
storagePwd,
timeMs,
nonce,
@@ -2737,8 +2768,8 @@ export class AuthService {
}
async deleteConversation({ login, toLogin, storagePwd, deleteByRecipient = false, timeMs = Date.now(), nonce = Math.floor(Math.random() * 0x100000000) }) {
const cleanLogin = String(login || '').trim();
const cleanPeerLogin = String(toLogin || '').trim();
const cleanLogin = this.normalizeDmLogin(login);
const cleanPeerLogin = this.normalizeDmLogin(toLogin);
if (!cleanLogin || !cleanPeerLogin) throw new Error('Не передан login/toLogin');
const normalizedTimeMs = Number(timeMs);
const normalizedNonce = Number(nonce);
+17 -4
View File
@@ -8,6 +8,10 @@ const DB_VERSION = 1;
const STORE_SECRETS = 'encrypted-secrets';
const STORE_SESSIONS = 'session-keys';
function normalizeLoginStorageKey(login) {
return String(login || '').trim().toLowerCase();
}
function openDb() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
@@ -50,15 +54,20 @@ async function get(storeName, key) {
export async function saveEncryptedUserSecrets(login, storagePwd, keys) {
const encrypted = await encryptJsonWithStoragePwd(keys, storagePwd);
const normalizedLogin = normalizeLoginStorageKey(login);
await put(STORE_SECRETS, {
login,
login: normalizedLogin,
encrypted,
updatedAtMs: Date.now(),
});
}
export async function loadEncryptedUserSecrets(login, storagePwd) {
const row = await get(STORE_SECRETS, login);
const normalizedLogin = normalizeLoginStorageKey(login);
let row = await get(STORE_SECRETS, normalizedLogin);
if (!row?.encrypted && normalizedLogin !== String(login || '').trim()) {
row = await get(STORE_SECRETS, String(login || '').trim());
}
if (!row?.encrypted) {
throw new Error('На устройстве нет сохранённых ключей для этого логина');
}
@@ -80,15 +89,19 @@ export async function updateEncryptedUserSecrets(login, storagePwd, updater) {
}
export async function saveSessionMaterial(login, material) {
const normalizedLogin = normalizeLoginStorageKey(login);
await put(STORE_SESSIONS, {
login,
login: normalizedLogin,
...material,
updatedAtMs: Date.now(),
});
}
export async function loadSessionMaterial(login) {
return get(STORE_SESSIONS, login);
const normalizedLogin = normalizeLoginStorageKey(login);
const row = await get(STORE_SESSIONS, normalizedLogin);
if (row || normalizedLogin === String(login || '').trim()) return row;
return get(STORE_SESSIONS, String(login || '').trim());
}
export async function clearClientAuthData() {
@@ -0,0 +1,108 @@
const SNAPSHOT_STORAGE_KEY = 'shine-ui-blockchain-snapshot-v1';
function normalizeLogin(login) {
return String(login || '').trim().toLowerCase();
}
function storageBucketKey(login) {
return normalizeLogin(login) || 'anonymous';
}
function decodeBase64ToBytes(base64) {
const raw = String(base64 || '').trim();
if (!raw) return new Uint8Array();
const bin = atob(raw);
const out = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i += 1) out[i] = bin.charCodeAt(i);
return out;
}
function readStorageMap() {
try {
const raw = localStorage.getItem(SNAPSHOT_STORAGE_KEY);
const parsed = raw ? JSON.parse(raw) : {};
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return {};
}
}
function writeStorageMap(value) {
try {
localStorage.setItem(SNAPSHOT_STORAGE_KEY, JSON.stringify(value || {}));
} catch {
// ignore localStorage errors
}
}
export function readStoredBlockchainSnapshot(login) {
const key = storageBucketKey(login);
const map = readStorageMap();
const value = map?.[key];
return value && typeof value === 'object' ? value : null;
}
export function saveStoredBlockchainSnapshot(login, snapshot) {
const key = storageBucketKey(login);
const map = readStorageMap();
map[key] = {
...snapshot,
savedAtMs: Date.now(),
};
writeStorageMap(map);
return map[key];
}
export function clearStoredBlockchainSnapshot(login) {
const key = storageBucketKey(login);
const map = readStorageMap();
delete map[key];
writeStorageMap(map);
}
export async function buildBlockchainSnapshotFile({
authService,
login,
blockchainName,
lastBlockNumber,
} = {}) {
const cleanLogin = normalizeLogin(login);
const cleanBlockchainName = String(blockchainName || '').trim();
const maxBlockNumber = Number(lastBlockNumber);
if (!authService?.ws?.request) throw new Error('Сервис сервера недоступен.');
if (!cleanLogin) throw new Error('Не указан логин.');
if (!cleanBlockchainName) throw new Error('Не указано имя блокчейна.');
if (!Number.isFinite(maxBlockNumber) || maxBlockNumber < 0) {
throw new Error('На сервере нет блоков для слепка.');
}
const parts = [];
let totalBytes = 0;
for (let blockNumber = 0; blockNumber <= maxBlockNumber; blockNumber += 1) {
const response = await authService.ws.request('GetBlockchainBlock', {
blockchainName: cleanBlockchainName,
blockNumber,
});
if (response?.status !== 200) {
const message = String(response?.payload?.message || response?.message || 'Не удалось получить блок.');
throw new Error(`Не удалось скачать блок ${blockNumber}: ${message}`);
}
const blockBytes = decodeBase64ToBytes(response?.payload?.blockBytesB64 || '');
parts.push(blockBytes);
totalBytes += blockBytes.length;
}
const file = new File(parts, `${cleanBlockchainName}.shine-blockchain`, {
type: 'application/octet-stream',
lastModified: Date.now(),
});
return {
file,
blockCount: maxBlockNumber + 1,
totalBytes,
lastBlockNumber: maxBlockNumber,
login: cleanLogin,
blockchainName: cleanBlockchainName,
};
}
+21 -10
View File
@@ -721,6 +721,8 @@ export async function getShineBlockchainUsage({ login, solanaEndpoint }) {
paidLimitBytes: bch.paidLimitBytes,
usedBytes: bch.usedBytes,
leftBytes,
blockchainName: String(bch.blockchainName || ''),
arweaveTxId: String(bch.arweaveTxId || ''),
lastBlockNumber: bch.lastBlockNumber,
lastBlockHashHex: Array.from(bch.lastBlockHash).map((x) => x.toString(16).padStart(2, '0')).join(''),
};
@@ -762,9 +764,10 @@ async function attachSolanaLogs(error, connection) {
}
async function buildCreateContext({ login, keyBundle, solanaEndpoint }) {
const rawLogin = String(login || '').trim();
const cleanLogin = normalizeLogin(login);
const endpoint = String(solanaEndpoint || '').trim();
if (!cleanLogin) throw new Error('Не указан логин');
if (!rawLogin || !cleanLogin) throw new Error('Не указан логин');
if (!endpoint) throw new Error('Не указан Solana RPC endpoint');
const solana = await loadSolanaLib();
@@ -791,6 +794,7 @@ async function buildCreateContext({ login, keyBundle, solanaEndpoint }) {
const clientKeypair = solana.Keypair.fromSeed(clientSeed32);
return {
rawLogin,
cleanLogin,
endpoint,
solana,
@@ -832,18 +836,19 @@ async function createShineUserPdaOnSolana({
}
const cleanLogin = ctx.cleanLogin;
const displayLogin = ctx.rawLogin;
const cleanPromoCode = String(promoCode || '').trim();
const blockchainName = `${cleanLogin}-001`;
const zeroHash32 = new Uint8Array(32);
const createdAtMs = BigInt(Date.now());
const startBonusLimit = parseUsersEconomyConfig(ecoAccount.data).startBonusLimit;
const lastBlockStateBytes = buildLastBlockStateBytes(cleanLogin, blockchainName, 0, zeroHash32, 0n);
const lastBlockStateBytes = buildLastBlockStateBytes(displayLogin, blockchainName, 0, zeroHash32, 0n);
const lastBlockStateHash = await sha256Bytes(lastBlockStateBytes);
const lastBlockSig64 = await signBytes(ctx.bchPrivKey, lastBlockStateHash);
const initialState = createPdaState({
login: cleanLogin,
login: displayLogin,
createdAtMs,
updatedAtMs: createdAtMs,
recordNumber: 0,
@@ -910,7 +915,7 @@ async function createShineUserPdaOnSolana({
});
}
const ixData = serializeCreateUserPdaArgs({
login: cleanLogin,
login: displayLogin,
recoveryKey32: ctx.recoveryKey32,
rootKey32: ctx.rootKey32,
createdAtMs,
@@ -1028,6 +1033,7 @@ export async function updateShineUserPdaOnSolana({
nextUsedBytes,
nextLastBlockNumber,
nextLastBlockHashHex,
nextArweaveTxId,
serverProfile,
accessServers,
trustedCount,
@@ -1043,6 +1049,7 @@ export async function updateShineUserPdaOnSolana({
const effectiveUsed = nextUsedBytes == null ? currentBch.usedBytes : BigInt(nextUsedBytes);
const effectiveLastNum = nextLastBlockNumber == null ? currentBch.lastBlockNumber : Number(nextLastBlockNumber);
const effectiveLastHash = parseHex32(nextLastBlockHashHex) || currentBch.lastBlockHash;
const effectiveArweaveTxId = nextArweaveTxId == null ? currentBch.arweaveTxId : String(nextArweaveTxId || '').trim();
if (effectiveLastHash.length !== 32) throw new Error('last block hash должен быть 32 байта');
const rootPriv = await importPkcs8Ed25519(rootPrivatePkcs8B64);
@@ -1060,7 +1067,7 @@ export async function updateShineUserPdaOnSolana({
const clientKeypair = solana.Keypair.fromSeed(clientSeed32);
const lastBlockStateBytes = buildLastBlockStateBytes(
cleanLogin,
current.login,
currentBch.blockchainName,
effectiveLastNum,
effectiveLastHash,
@@ -1085,7 +1092,10 @@ export async function updateShineUserPdaOnSolana({
const updatedAtMs = BigInt(Date.now());
const newPaid = currentBch.paidLimitBytes + addLimit;
const newRecordNumber = current.recordNumber + 1;
const prevHash = await sha256Bytes(serializeUnsignedRecordFromState(current));
// Для prev_hash нужно хэшировать точную unsigned-часть текущей PDA,
// а не пересобирать её из распарсенного состояния: иначе можно получить
// несовпадение байт и InvalidPrevHash в on-chain программе.
const prevHash = await sha256Bytes(current.unsignedBytes || serializeUnsignedRecordFromState(current));
const nextServerProfile = serverProfile
? {
@@ -1097,7 +1107,7 @@ export async function updateShineUserPdaOnSolana({
: current.serverProfile;
const nextState = createPdaState({
login: cleanLogin,
login: current.login,
createdAtMs: current.createdAtMs,
updatedAtMs,
recordNumber: newRecordNumber,
@@ -1113,7 +1123,7 @@ export async function updateShineUserPdaOnSolana({
lastBlockNumber: effectiveLastNum,
lastBlockHash: effectiveLastHash,
lastBlockSignature: lastBlockSig64,
arweaveTxId: currentBch.arweaveTxId,
arweaveTxId: effectiveArweaveTxId,
}),
isServer: Boolean(nextServerProfile),
addressFormatType: nextServerProfile?.addressFormatType ?? 0,
@@ -1131,7 +1141,7 @@ export async function updateShineUserPdaOnSolana({
const rootSig64 = await signBytes(rootPriv, unsignedNextHash);
const ixData = serializeUpdateUserPdaArgs({
login: cleanLogin,
login: current.login,
recoveryKey32: current.recoveryKey,
rootKey32: current.rootKey,
createdAtMs: current.createdAtMs,
@@ -1146,7 +1156,7 @@ export async function updateShineUserPdaOnSolana({
lastBlockNumber: effectiveLastNum,
lastBlockHash32: effectiveLastHash,
lastBlockSignature64: lastBlockSig64,
arweaveTxId: currentBch.arweaveTxId,
arweaveTxId: effectiveArweaveTxId,
isServer: nextState.isServer,
addressFormatType: nextState.addressFormatType,
addressFormatVersion: nextState.addressFormatVersion,
@@ -1205,6 +1215,7 @@ export async function updateShineUserPdaOnSolana({
leftBytes: newPaid > effectiveUsed ? (newPaid - effectiveUsed) : 0n,
lastBlockNumber: effectiveLastNum,
lastBlockHashHex: Array.from(effectiveLastHash).map((x) => x.toString(16).padStart(2, '0')).join(''),
arweaveTxId: effectiveArweaveTxId,
};
}