Files
ai-qpid-pvz/game/js/systems/auth.js
T

282 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Auth & Profile Management System — Server-backed (SQLite via /api/auth/)
// Falls back to localStorage if server is unavailable.
window.PvZAuth = (function () {
const CURRENT_USER_KEY = 'pvz_current_user';
const LOCAL_DATA_KEY = 'pvz_local_data'; // local fallback cache
const API_BASE = '/api/auth';
/* ─── Password hashing (djb2 for lightweight fast client hashing) ─── */
function hashPassword(str) {
if (!str) return '';
let h = 0;
for (let i = 0; i < str.length; i++) {
h = ((h << 5) - h) + str.charCodeAt(i);
h |= 0;
}
return h.toString(16);
}
/* ─── Validation Helpers ─── */
function validateUsername(username) {
const u = (username || '').trim();
if (!u) return { valid: false, message: 'Введите имя пользователя' };
if (u.length < 3) return { valid: false, message: 'Логин должен содержать минимум 3 символа' };
if (u.length > 20) return { valid: false, message: 'Логин не должен превышать 20 символов' };
if (!/^[a-zA-Zа-яА-ЯёЁ0-9_-]+$/.test(u)) {
return { valid: false, message: 'Логин может содержать только буквы, цифры, "-" и "_"' };
}
const reserved = ['гость', 'guest', 'admin', 'null', 'undefined', 'system', 'anonymous'];
if (reserved.includes(u.toLowerCase())) {
return { valid: false, message: 'Это имя пользователя зарезервировано' };
}
return { valid: true, username: u };
}
function validatePassword(password) {
const p = password || '';
if (!p) return { valid: false, message: 'Введите пароль' };
if (p.length < 4) return { valid: false, message: 'Пароль должен содержать не менее 4 символов' };
return { valid: true, password: p };
}
/* ─── Default blank profile ─── */
function getDefaultData() {
return {
coins: 150,
level: 1,
maxUnlockedLevel: 1,
highScore: 0,
theme: 'classic',
upgrades: { extraSlot: false, hasRake: false },
unlockedPlants: ['sunflower', 'peashooter', 'wallnut'],
selectedDeck: ['sunflower', 'peashooter', 'wallnut'],
materials: { bioHumus: 10, scrapMetal: 5, sunEssence: 20, crystals: 0 },
plantLevels: { peashooter: 1, sunflower: 1, wallnut: 1, potatomine: 1,
repeater: 1, snowpea: 1, chomper: 1 },
perks: { magnetism: 0, startSun: 0, fastCool: 0, hardyRoots: 0, luckyDrop: 0 },
zenGarden: {
pots: [
{ id: 0, plantType: 'sunflower', stage: 2, thirstTimer: 0, needsFertilizer: false, happyTimer: 60, coinTimer: 5 },
{ id: 1, plantType: 'peashooter', stage: 1, thirstTimer: 0, needsFertilizer: false, happyTimer: 45, coinTimer: 8 },
{ id: 2, plantType: null, stage: 0, thirstTimer: 0, needsFertilizer: false, happyTimer: 0, coinTimer: 20 },
{ id: 3, plantType: null, stage: 0, thirstTimer: 0, needsFertilizer: false, happyTimer: 0, coinTimer: 20 },
{ id: 4, plantType: null, stage: 0, thirstTimer: 0, needsFertilizer: false, happyTimer: 0, coinTimer: 20 },
{ id: 5, plantType: null, stage: 0, thirstTimer: 0, needsFertilizer: false, happyTimer: 0, coinTimer: 20 }
],
fertilizerCount: 5,
chocolateCount: 2,
snail: { x: 100, targetX: 300, dir: 1, isAwake: true, sleepTimer: 120, boostTimer: 0 },
lastVisit: Date.now()
},
treeOfWisdom: { height: 0, cheats: {} },
achievements: {}
};
}
/* ─── In-memory current user data ─── */
let _cachedData = null;
function localSave(data) {
try { localStorage.setItem(LOCAL_DATA_KEY, JSON.stringify(data)); } catch (_) {}
}
function localLoad() {
try { return JSON.parse(localStorage.getItem(LOCAL_DATA_KEY) || 'null'); } catch (_) { return null; }
}
/* ─── Non-blocking server call ─── */
async function apiPost(endpoint, body) {
try {
const resp = await fetch(`${API_BASE}/${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const json = await resp.json();
return json;
} catch (e) {
return { success: false, message: 'Сервер авторизации недоступен' };
}
}
/* ─── Debounced save (prevents save storms on rapid updates) ─── */
let _saveTimer = null;
function scheduleSave() {
if (_saveTimer) clearTimeout(_saveTimer);
_saveTimer = setTimeout(() => {
const user = localStorage.getItem(CURRENT_USER_KEY);
if (!user || user === 'Гость' || !_cachedData) return;
apiPost('save', { username: user, data: _cachedData });
}, 1500);
}
function mergeUserData(serverData) {
const def = getDefaultData();
if (!serverData) return def;
return {
...def,
...serverData,
upgrades: { ...def.upgrades, ...(serverData.upgrades || {}) },
unlockedPlants: serverData.unlockedPlants || def.unlockedPlants,
selectedDeck: serverData.selectedDeck || def.selectedDeck,
materials: { ...def.materials, ...(serverData.materials || {}) },
plantLevels: { ...def.plantLevels, ...(serverData.plantLevels || {}) },
perks: { ...def.perks, ...(serverData.perks || {}) },
zenGarden: serverData.zenGarden
? {
...def.zenGarden,
...serverData.zenGarden,
pots: serverData.zenGarden.pots || def.zenGarden.pots,
snail: { ...def.zenGarden.snail, ...(serverData.zenGarden.snail || {}) }
}
: def.zenGarden,
treeOfWisdom: serverData.treeOfWisdom || def.treeOfWisdom,
achievements: serverData.achievements || {}
};
}
return {
/* ── Public API ── */
getCurrentUser() {
return localStorage.getItem(CURRENT_USER_KEY) || 'Гость';
},
isGuest() {
const u = localStorage.getItem(CURRENT_USER_KEY);
return !u || u === 'Гость';
},
getDefaultData,
validateUsername,
validatePassword,
apiPost,
scheduleSave,
/**
* User Login
*/
async login(username, password, callback = null) {
const userVal = validateUsername(username);
if (!userVal.valid) {
if (typeof callback === 'function') callback(false, null, userVal.message);
return { success: false, message: userVal.message };
}
const passVal = validatePassword(password);
if (!passVal.valid) {
if (typeof callback === 'function') callback(false, null, passVal.message);
return { success: false, message: passVal.message };
}
const cleanUser = userVal.username;
const pwdHash = hashPassword(passVal.password);
const res = await apiPost('login', { username: cleanUser, passwordHash: pwdHash });
if (res && res.success && res.data) {
localStorage.setItem(CURRENT_USER_KEY, cleanUser);
_cachedData = mergeUserData(res.data);
localSave(_cachedData);
if (window.GameEngine && window._pvzEngine) {
window._pvzEngine.loadUserData();
}
if (typeof callback === 'function') callback(true, _cachedData, null);
return { success: true, username: cleanUser, data: _cachedData };
} else {
const errorMsg = (res && res.message) ? res.message : 'Неверный логин или пароль';
if (typeof callback === 'function') callback(false, null, errorMsg);
return { success: false, message: errorMsg };
}
},
/**
* User Registration
*/
async register(username, password, callback = null) {
const userVal = validateUsername(username);
if (!userVal.valid) {
if (typeof callback === 'function') callback(false, null, userVal.message);
return { success: false, message: userVal.message };
}
const passVal = validatePassword(password);
if (!passVal.valid) {
if (typeof callback === 'function') callback(false, null, passVal.message);
return { success: false, message: passVal.message };
}
const cleanUser = userVal.username;
const pwdHash = hashPassword(passVal.password);
const initData = getDefaultData();
const res = await apiPost('register', { username: cleanUser, passwordHash: pwdHash, initialData: initData });
if (res && res.success) {
localStorage.setItem(CURRENT_USER_KEY, cleanUser);
_cachedData = mergeUserData(res.data || initData);
localSave(_cachedData);
if (window.GameEngine && window._pvzEngine) {
window._pvzEngine.loadUserData();
}
if (typeof callback === 'function') callback(true, _cachedData, null);
return { success: true, username: cleanUser, data: _cachedData };
} else {
const errorMsg = (res && res.message) ? res.message : 'Ошибка при регистрации';
if (typeof callback === 'function') callback(false, null, errorMsg);
return { success: false, message: errorMsg };
}
},
loginAsGuest() {
localStorage.setItem(CURRENT_USER_KEY, 'Гость');
_cachedData = null;
localSave(null);
return { success: true, user: 'Гость' };
},
logout() {
if (_saveTimer) { clearTimeout(_saveTimer); _saveTimer = null; }
const user = this.getCurrentUser();
if (user !== 'Гость' && _cachedData) {
apiPost('save', { username: user, data: _cachedData });
}
this.loginAsGuest();
},
getUserData() {
if (this.isGuest()) return getDefaultData();
if (_cachedData) return _cachedData;
const local = localLoad();
if (local) {
_cachedData = local;
return local;
}
_cachedData = getDefaultData();
return _cachedData;
},
saveUserData(data) {
if (this.isGuest()) return; // Guests are not persisted
_cachedData = _cachedData
? { ..._cachedData, ...data }
: { ...getDefaultData(), ...data };
localSave(_cachedData); // instant local write
scheduleSave(); // debounced server sync
},
/** Force immediate server save (call on game-over / victory) */
flushSave() {
if (this.isGuest() || !_cachedData) return;
if (_saveTimer) { clearTimeout(_saveTimer); _saveTimer = null; }
apiPost('save', { username: this.getCurrentUser(), data: _cachedData });
}
};
})();