feat(auth): enforce tamper-proof cloud saves and remove local export/import (fixes #4)

This commit is contained in:
Antigravity committed 2026-09-22 11:18:31 +00:00
1 parent 198d1dcf98
commit 9f905569a5
4 files changed
+40 -175

No files matched your search

+6 -133
View File
@@ -578,146 +578,19 @@ window.PvZAuth = (function () {
},
/**
* Export all save data (Profile, levels, shop, garden, scores) to a JSON file
* Export save file - disabled in favor of secure cloud saves
*/
exportSaveFile() {
try {
if (window._pvzEngine && typeof window._pvzEngine.saveUserData === 'function') {
window._pvzEngine.saveUserData();
}
const data = this.getUserData();
const username = this.getCurrentUser();
const isGuest = this.isGuest();
const highScores = {
adventure: data.highScore || 0,
survival: parseInt(localStorage.getItem('pvz_hs_survival') || '0', 10),
bowling: parseInt(localStorage.getItem('pvz_hs_bowling') || '0', 10),
izombie: parseInt(localStorage.getItem('pvz_hs_izombie') || '0', 10),
rogue: parseInt(localStorage.getItem('pvz_hs_rogue') || '0', 10),
boss: parseInt(localStorage.getItem('pvz_hs_boss') || '0', 10),
bossrush: parseInt(localStorage.getItem('pvz_hs_bossrush') || '0', 10)
};
const exportPayload = {
schema: 'pvz_save_file',
version: 2,
exportedAt: new Date().toISOString(),
username: isGuest ? 'Гость' : username,
data: {
...data,
highScores
}
};
const jsonStr = JSON.stringify(exportPayload, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json;charset=utf-8' });
const url = URL.createObjectURL(blob);
const now = new Date();
const dateStr = now.toISOString().slice(0, 10);
const safeUser = (username || 'guest').replace(/[^a-zA-Zа-яА-Я0-9_-]/g, '_');
const filename = `pvz_save_${safeUser}_${dateStr}.json`;
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 1000);
showNotification('Файл сохранения скачан!', filename, '💾');
if (window.PvZAudio && window.PvZAudio.playCoin) window.PvZAudio.playCoin();
return { success: true, filename };
} catch (err) {
console.error('Export save error:', err);
showNotification('Ошибка экспорта', err.message || 'Сбой', '❌');
return { success: false, message: err.message };
}
showNotification('Облачное сохранение', 'Все данные сохраняются на сервере. Экспорт в файл отключен.', '☁️');
return { success: false, message: 'Локальный экспорт отключен' };
},
/**
* Import save data from JSON file with validation & UI refresh
* Import save file - disabled in favor of secure cloud saves
*/
async importSaveFile(file) {
if (!file) {
showNotification('Файл не выбран', 'Выберите .json файл', '⚠️');
return { success: false, message: 'Файл не выбран' };
}
try {
const text = await file.text();
let parsed;
try {
parsed = JSON.parse(text);
} catch (e) {
showNotification('Некорректный JSON', 'Файл поврежден или не является JSON', '❌');
return { success: false, message: 'Некорректный JSON формат файла' };
}
if (!parsed || typeof parsed !== 'object') {
showNotification('Неверный формат', 'Файл пуст или некорректен', '❌');
return { success: false, message: 'Неверная структура данных' };
}
// Support both wrapped export payload and raw data format
let rawData = (parsed.schema === 'pvz_save_file' && parsed.data) ? parsed.data : (parsed.data || parsed);
if (typeof rawData !== 'object' || rawData === null) {
showNotification('Ошибка в данных', 'Внутри файла нет объекта сохранений', '❌');
return { success: false, message: 'Некорректный объект данных' };
}
// Field sanitization & validation
if (rawData.coins !== undefined && (typeof rawData.coins !== 'number' || isNaN(rawData.coins) || rawData.coins < 0)) {
rawData.coins = 150;
}
if (rawData.level !== undefined && (typeof rawData.level !== 'number' || isNaN(rawData.level) || rawData.level < 1)) {
rawData.level = 1;
}
if (rawData.maxUnlockedLevel !== undefined && (typeof rawData.maxUnlockedLevel !== 'number' || isNaN(rawData.maxUnlockedLevel) || rawData.maxUnlockedLevel < 1)) {
rawData.maxUnlockedLevel = rawData.level || 1;
}
// Merge with defaults to guarantee all required properties exist
_cachedData = mergeUserData(rawData);
localSave(_cachedData);
// Restore individual high score keys if present
if (rawData.highScores && typeof rawData.highScores === 'object') {
if (rawData.highScores.survival !== undefined) localStorage.setItem('pvz_hs_survival', String(rawData.highScores.survival));
if (rawData.highScores.bowling !== undefined) localStorage.setItem('pvz_hs_bowling', String(rawData.highScores.bowling));
if (rawData.highScores.izombie !== undefined) localStorage.setItem('pvz_hs_izombie', String(rawData.highScores.izombie));
if (rawData.highScores.rogue !== undefined) localStorage.setItem('pvz_hs_rogue', String(rawData.highScores.rogue));
if (rawData.highScores.boss !== undefined) localStorage.setItem('pvz_hs_boss', String(rawData.highScores.boss));
if (rawData.highScores.bossrush !== undefined) localStorage.setItem('pvz_hs_bossrush', String(rawData.highScores.bossrush));
}
// Sync to server if user is logged in
if (!this.isGuest()) {
this.flushSave();
}
// Reload state into game engine
if (window._pvzEngine) {
window._pvzEngine.loadUserData();
window._pvzEngine.updateProfileUI();
window._pvzEngine.updateRecordDisplay();
window._pvzEngine.updateShopUI();
}
showNotification('Сохранение загружено!', `Баланс: ${_cachedData.coins} 💰, Уровень ${_cachedData.level}`, '✅');
if (window.PvZAudio && window.PvZAudio.playCoin) window.PvZAudio.playCoin();
return { success: true, data: _cachedData };
} catch (err) {
console.error('Import save error:', err);
showNotification('Ошибка импорта', err.message || 'Не удалось прочитать файл', '❌');
return { success: false, message: err.message };
}
showNotification('Облачный профиль', 'Импорт локальных файлов отключен. Прогресс загружается с сервера.', '🔒');
return { success: false, message: 'Локальный импорт отключен' };
}
};