feat(auth): Separate Login, Register, and Profile views with robust validations and SQL-safe backend

This commit is contained in:
2026-09-17 10:22:02 +00:00
parent 5f3abba27c
commit 4e450d758b
5 changed files with 392 additions and 126 deletions
+122 -54
View File
@@ -5,8 +5,9 @@ window.PvZAuth = (function () {
const LOCAL_DATA_KEY = 'pvz_local_data'; // local fallback cache
const API_BASE = '/api/auth';
/* ─── Simple djb2 hash (same as before, for backward-compat) ─── */
/* ─── 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);
@@ -15,6 +16,29 @@ window.PvZAuth = (function () {
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 {
@@ -65,9 +89,10 @@ window.PvZAuth = (function () {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
return await resp.json();
const json = await resp.json();
return json;
} catch (e) {
return { success: false, message: 'Сервер недоступен' };
return { success: false, message: 'Сервер авторизации недоступен' };
}
}
@@ -82,6 +107,29 @@ window.PvZAuth = (function () {
}, 1500);
}
function mergeUserData(serverData) {
const def = getDefaultData();
if (!serverData) return def;
return {
...def,
...serverData,
upgrades: { ...def.upgrades, ...(serverData.upgrades || {}) },
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 ── */
@@ -95,64 +143,86 @@ window.PvZAuth = (function () {
},
getDefaultData,
validateUsername,
validatePassword,
apiPost,
scheduleSave,
/**
* Login or auto-register.
* Synchronous return with provisional localStorage data;
* server response loaded asynchronously and applied via callback.
* User Login
*/
login(username, password, onServerResponse = null) {
username = (username || '').trim();
if (!username) return { success: false, message: 'Введите имя пользователя' };
if (username.toLowerCase() === 'гость') {
this.loginAsGuest();
return { success: true, user: 'Гость' };
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 pwdHash = hashPassword(password);
const passVal = validatePassword(password);
if (!passVal.valid) {
if (typeof callback === 'function') callback(false, null, passVal.message);
return { success: false, message: passVal.message };
}
// Optimistically set local session
localStorage.setItem(CURRENT_USER_KEY, username);
_cachedData = getDefaultData();
localSave(_cachedData);
const cleanUser = userVal.username;
const pwdHash = hashPassword(passVal.password);
// Async server auth
apiPost('login', { username, passwordHash: pwdHash }).then(res => {
if (res && res.success && res.data) {
// Merge defaults + server data
const def = getDefaultData();
_cachedData = {
...def, ...res.data,
upgrades: { ...def.upgrades, ...(res.data.upgrades || {}) },
materials: { ...def.materials, ...(res.data.materials || {}) },
plantLevels: { ...def.plantLevels, ...(res.data.plantLevels || {}) },
perks: { ...def.perks, ...(res.data.perks || {}) },
zenGarden: res.data.zenGarden
? { ...def.zenGarden, ...res.data.zenGarden,
pots: res.data.zenGarden.pots || def.zenGarden.pots,
snail: { ...def.zenGarden.snail, ...(res.data.zenGarden.snail || {}) }
}
: def.zenGarden,
treeOfWisdom: res.data.treeOfWisdom || def.treeOfWisdom,
achievements: res.data.achievements || {}
};
localSave(_cachedData);
if (typeof onServerResponse === 'function') onServerResponse(_cachedData);
// Notify game engine to reload profile
if (window.GameEngine && window._pvzEngine) {
window._pvzEngine.loadUserData();
}
} else if (res && !res.success) {
// Wrong password — kick back to guest
if (res.message && res.message.includes('пароль')) {
localStorage.setItem(CURRENT_USER_KEY, 'Гость');
_cachedData = null;
if (typeof onServerResponse === 'function') onServerResponse(null, res.message);
}
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 };
}
},
return { success: true, user: username, pending: true };
/**
* 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() {
@@ -163,7 +233,6 @@ window.PvZAuth = (function () {
},
logout() {
// Flush any pending save first
if (_saveTimer) { clearTimeout(_saveTimer); _saveTimer = null; }
const user = this.getCurrentUser();
if (user !== 'Гость' && _cachedData) {
@@ -177,7 +246,6 @@ window.PvZAuth = (function () {
if (_cachedData) return _cachedData;
// Try local cache (sync fallback while server loads)
const local = localLoad();
if (local) {
_cachedData = local;