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

332 lines
12 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 TOKEN_KEY = 'pvz_token';
const LOCAL_DATA_KEY = 'pvz_local_data'; // local fallback cache
const API_BASE = '/api/auth';
/* ─── Session Token Helpers ─── */
function getToken() {
try { return localStorage.getItem(TOKEN_KEY) || null; } catch (_) { return null; }
}
function setToken(token) {
try {
if (token) localStorage.setItem(TOKEN_KEY, token);
else localStorage.removeItem(TOKEN_KEY);
} catch (_) {}
}
function clearToken() {
try { localStorage.removeItem(TOKEN_KEY); } catch (_) {}
}
/* ─── 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 with Bearer Token Authorization ─── */
async function apiPost(endpoint, body = {}) {
try {
const headers = { 'Content-Type': 'application/json' };
const token = getToken();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
// Attach token in JSON body as well for maximum compatibility
const payload = (typeof body === 'object' && body !== null && token)
? { token, ...body }
: body;
const resp = await fetch(`${API_BASE}/${endpoint}`, {
method: 'POST',
headers,
body: JSON.stringify(payload)
});
if (resp.status === 401) {
// Clear invalid or expired session token
clearToken();
const json = await resp.json().catch(() => ({}));
return { success: false, status: 401, message: json.message || 'Сессия истекла или недействительна' };
}
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 === 'Гость';
},
getToken,
setToken,
clearToken,
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);
if (res.token) {
setToken(res.token);
}
_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, token: res.token, 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);
if (res.token) {
setToken(res.token);
}
_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, token: res.token, data: _cachedData };
} else {
const errorMsg = (res && res.message) ? res.message : 'Ошибка при регистрации';
if (typeof callback === 'function') callback(false, null, errorMsg);
return { success: false, message: errorMsg };
}
},
loginAsGuest() {
clearToken();
localStorage.setItem(CURRENT_USER_KEY, 'Гость');
_cachedData = null;
localSave(null);
return { success: true, user: 'Гость' };
},
logout() {
if (_saveTimer) { clearTimeout(_saveTimer); _saveTimer = null; }
const user = this.getCurrentUser();
const token = getToken();
if (user !== 'Гость' && _cachedData) {
apiPost('save', { username: user, data: _cachedData });
}
if (token) {
apiPost('logout', { token });
}
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 });
}
};
})();