-
+
👤 ЛИЧНЫЙ ПРОФИЛЬ
-
-
-
ВХОД В ПРОФИЛЬ
+
+
+
🔑 ВХОД В АККАУНТ
- Войдите или зарегистрируйтесь, чтобы сохранять монеты, крафт и уровень кампании на сервере.
+ Войдите под своим логином для синхронизации сохранений с сервером.
-
diff --git a/game/js/game.js b/game/js/game.js
index e2994b4..b1fdf0b 100644
--- a/game/js/game.js
+++ b/game/js/game.js
@@ -206,7 +206,8 @@ window.GameEngine = class GameEngine {
const btnAuth = document.getElementById('btn-auth-open');
const btnLogoutNav = document.getElementById('btn-auth-logout-nav');
const loggedInView = document.getElementById('auth-logged-in-view');
- const guestView = document.getElementById('auth-guest-view');
+ const loginView = document.getElementById('auth-login-view') || document.getElementById('auth-guest-view');
+ const registerView = document.getElementById('auth-register-view');
const userTitle = document.getElementById('profile-user-title');
const statCoins = document.getElementById('profile-stat-coins');
const statLevel = document.getElementById('profile-stat-level');
@@ -226,13 +227,15 @@ window.GameEngine = class GameEngine {
else btnLogoutNav.classList.remove('hidden');
}
- if (loggedInView && guestView) {
+ if (loggedInView) {
if (isGuest) {
loggedInView.classList.add('hidden');
- guestView.classList.remove('hidden');
+ if (loginView) loginView.classList.remove('hidden');
+ if (registerView) registerView.classList.add('hidden');
} else {
loggedInView.classList.remove('hidden');
- guestView.classList.add('hidden');
+ if (loginView) loginView.classList.add('hidden');
+ if (registerView) registerView.classList.add('hidden');
if (userTitle) userTitle.textContent = user;
if (statCoins) statCoins.textContent = (uData.coins || (this.economy ? this.economy.totalCoins : 0)).toLocaleString('ru-RU');
@@ -897,14 +900,37 @@ window.GameEngine = class GameEngine {
const btnAuthOpen = document.getElementById('btn-auth-open');
const btnAuthClose = document.getElementById('btn-auth-close');
const btnAuthCloseProfile = document.getElementById('btn-auth-close-profile');
- const btnAuthSubmit = document.getElementById('btn-auth-submit');
- const btnAuthGuest = document.getElementById('btn-auth-guest');
- const btnAuthLogout = document.getElementById('btn-auth-logout');
+ const btnAuthCloseReg = document.getElementById('btn-auth-close-reg');
+
+ const loginView = document.getElementById('auth-login-view') || document.getElementById('auth-guest-view');
+ const registerView = document.getElementById('auth-register-view');
+
+ const btnAuthToRegister = document.getElementById('btn-auth-to-register');
+ const btnAuthToLogin = document.getElementById('btn-auth-to-login');
+
+ const btnAuthSubmit = document.getElementById('btn-auth-submit'); // Войти
+ const btnAuthDoRegister = document.getElementById('btn-auth-do-register'); // Зарегистрироваться
+ const btnAuthGuest = document.getElementById('btn-auth-guest'); // Остаться гостем
+ const btnAuthLogout = document.getElementById('btn-auth-logout'); // Выйти
const btnAuthLogoutNav = document.getElementById('btn-auth-logout-nav');
+
const authUser = document.getElementById('auth-username');
const authPass = document.getElementById('auth-password');
const authError = document.getElementById('auth-error');
+ const authRegUser = document.getElementById('auth-reg-username');
+ const authRegPass = document.getElementById('auth-reg-password');
+ const authRegError = document.getElementById('auth-reg-error');
+
+ const resetAuthForms = () => {
+ if (authUser) authUser.value = '';
+ if (authPass) authPass.value = '';
+ if (authError) { authError.textContent = ''; authError.classList.add('hidden'); }
+ if (authRegUser) authRegUser.value = '';
+ if (authRegPass) authRegPass.value = '';
+ if (authRegError) { authRegError.textContent = ''; authRegError.classList.add('hidden'); }
+ };
+
const handleLogout = () => {
if (window.PvZAuth) {
window.PvZAuth.logout();
@@ -914,9 +940,7 @@ window.GameEngine = class GameEngine {
this.updateShopUI();
}
if (authModal) authModal.classList.add('hidden');
- if (authUser) authUser.value = '';
- if (authPass) authPass.value = '';
- if (authError) authError.classList.add('hidden');
+ resetAuthForms();
if (window.PvZAudio) window.PvZAudio.playCoin();
};
@@ -929,15 +953,20 @@ window.GameEngine = class GameEngine {
if (btnAuthOpen && authModal) {
btnAuthOpen.addEventListener('click', () => {
- if (authError) authError.classList.add('hidden');
+ resetAuthForms();
this.updateProfileUI();
authModal.classList.remove('hidden');
- if (authUser && window.PvZAuth && window.PvZAuth.isGuest()) authUser.focus();
+ if (authUser && window.PvZAuth && window.PvZAuth.isGuest()) {
+ setTimeout(() => authUser.focus(), 50);
+ }
});
}
+
+ // Modal Close buttons
if (btnAuthClose && authModal) {
btnAuthClose.addEventListener('click', () => {
authModal.classList.add('hidden');
+ resetAuthForms();
});
}
if (btnAuthCloseProfile && authModal) {
@@ -945,61 +974,80 @@ window.GameEngine = class GameEngine {
authModal.classList.add('hidden');
});
}
- if (btnAuthGuest && authModal) {
- btnAuthGuest.addEventListener('click', () => {
- handleLogout();
+ if (btnAuthCloseReg && authModal) {
+ btnAuthCloseReg.addEventListener('click', () => {
+ authModal.classList.add('hidden');
+ resetAuthForms();
});
}
+
+ // Switch between Login and Register views
+ if (btnAuthToRegister && loginView && registerView) {
+ btnAuthToRegister.addEventListener('click', () => {
+ resetAuthForms();
+ loginView.classList.add('hidden');
+ registerView.classList.remove('hidden');
+ if (authRegUser) setTimeout(() => authRegUser.focus(), 50);
+ });
+ }
+
+ if (btnAuthToLogin && loginView && registerView) {
+ btnAuthToLogin.addEventListener('click', () => {
+ resetAuthForms();
+ registerView.classList.add('hidden');
+ loginView.classList.remove('hidden');
+ if (authUser) setTimeout(() => authUser.focus(), 50);
+ });
+ }
+
+ // Stay as Guest button
+ if (btnAuthGuest && authModal) {
+ btnAuthGuest.addEventListener('click', () => {
+ if (window.PvZAuth) window.PvZAuth.loginAsGuest();
+ this.loadUserData();
+ this.updateProfileUI();
+ this.updateRecordDisplay();
+ authModal.classList.add('hidden');
+ resetAuthForms();
+ if (window.PvZAudio) window.PvZAudio.playButtonClick();
+ });
+ }
+
+ // Login Action (Войти)
if (btnAuthSubmit && authModal) {
- btnAuthSubmit.addEventListener('click', () => {
+ btnAuthSubmit.addEventListener('click', async () => {
const u = authUser ? authUser.value.trim() : '';
const p = authPass ? authPass.value : '';
+
if (!u) {
- if (authError) { authError.textContent = 'Введите имя пользователя'; authError.classList.remove('hidden'); }
+ if (authError) { authError.textContent = 'Введите логин'; authError.classList.remove('hidden'); }
return;
}
- if (!p && u.toLowerCase() !== 'гость') {
+ if (!p) {
if (authError) { authError.textContent = 'Введите пароль'; authError.classList.remove('hidden'); }
return;
}
if (authError) authError.classList.add('hidden');
if (window.PvZAuth) {
- btnAuthSubmit.textContent = '⏳ Подключение...';
+ btnAuthSubmit.textContent = '⏳ Вход...';
btnAuthSubmit.disabled = true;
- const res = window.PvZAuth.login(u, p, (serverData, errMsg) => {
- // Server response callback (async)
- btnAuthSubmit.textContent = 'Войти / Регистрация';
- btnAuthSubmit.disabled = false;
- if (errMsg) {
- // Wrong password from server
- if (authError) { authError.textContent = errMsg; authError.classList.remove('hidden'); }
- // Restore guest state in UI
- this.updateProfileUI();
- return;
- }
- if (serverData) {
- this.loadUserData();
- this.updateProfileUI();
- this.updateRecordDisplay();
- this.updateShopUI();
- }
- });
+ const res = await window.PvZAuth.login(u, p);
+ btnAuthSubmit.textContent = '🔑 Войти';
+ btnAuthSubmit.disabled = false;
- if (res.success) {
- // Optimistic: close modal immediately (data loads async)
+ if (res && res.success) {
authModal.classList.add('hidden');
- if (authUser) authUser.value = '';
- if (authPass) authPass.value = '';
+ resetAuthForms();
this.loadUserData();
this.updateProfileUI();
this.updateRecordDisplay();
+ this.updateShopUI();
+ if (window.PvZAudio) window.PvZAudio.playCoin();
} else {
- btnAuthSubmit.textContent = 'Войти / Регистрация';
- btnAuthSubmit.disabled = false;
if (authError) {
- authError.textContent = res.message || 'Ошибка входа';
+ authError.textContent = (res && res.message) ? res.message : 'Неверный логин или пароль';
authError.classList.remove('hidden');
}
}
@@ -1007,6 +1055,52 @@ window.GameEngine = class GameEngine {
});
}
+ // Register Action (Зарегистрироваться)
+ if (btnAuthDoRegister && authModal) {
+ btnAuthDoRegister.addEventListener('click', async () => {
+ const u = authRegUser ? authRegUser.value.trim() : '';
+ const p = authRegPass ? authRegPass.value : '';
+
+ if (!u) {
+ if (authRegError) { authRegError.textContent = 'Введите логин для регистрации'; authRegError.classList.remove('hidden'); }
+ return;
+ }
+ if (u.length < 3) {
+ if (authRegError) { authRegError.textContent = 'Логин должен содержать минимум 3 символа'; authRegError.classList.remove('hidden'); }
+ return;
+ }
+ if (!p || p.length < 4) {
+ if (authRegError) { authRegError.textContent = 'Пароль должен содержать минимум 4 символа'; authRegError.classList.remove('hidden'); }
+ return;
+ }
+ if (authRegError) authRegError.classList.add('hidden');
+
+ if (window.PvZAuth) {
+ btnAuthDoRegister.textContent = '⏳ Регистрация...';
+ btnAuthDoRegister.disabled = true;
+
+ const res = await window.PvZAuth.register(u, p);
+ btnAuthDoRegister.textContent = '✨ Зарегистрироваться';
+ btnAuthDoRegister.disabled = false;
+
+ if (res && res.success) {
+ authModal.classList.add('hidden');
+ resetAuthForms();
+ this.loadUserData();
+ this.updateProfileUI();
+ this.updateRecordDisplay();
+ this.updateShopUI();
+ if (window.PvZAudio) window.PvZAudio.playCoin();
+ } else {
+ if (authRegError) {
+ authRegError.textContent = (res && res.message) ? res.message : 'Ошибка при регистрации';
+ authRegError.classList.remove('hidden');
+ }
+ }
+ }
+ });
+ }
+
// Shop Button
const btnShop = document.getElementById('btn-shop');
diff --git a/game/js/systems/auth.js b/game/js/systems/auth.js
index 0a762a4..11603dd 100644
--- a/game/js/systems/auth.js
+++ b/game/js/systems/auth.js
@@ -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;
diff --git a/scripts/feedback_server.py b/scripts/feedback_server.py
index 1d5e3e3..4a8906a 100755
--- a/scripts/feedback_server.py
+++ b/scripts/feedback_server.py
@@ -164,8 +164,53 @@ class FeedbackHandler(http.server.BaseHTTPRequestHandler):
self.wfile.write(json.dumps({"error": f"Invalid JSON: {e}"}).encode("utf-8"))
return
- # 1. User Authentication (Login & Auto-Registration)
- if path in ["/api/auth/login", "/api/bugs/auth/login", "/api/auth/register"]:
+ # 1a. User Login
+ if path in ["/api/auth/login", "/api/bugs/auth/login"]:
+ username = str(payload.get("username", "")).strip()
+ password_hash = str(payload.get("passwordHash", "")).strip()
+
+ if not username or not password_hash:
+ self.send_response(400)
+ self._set_cors()
+ self.end_headers()
+ self.wfile.write(json.dumps({"success": False, "message": "Введите логин и пароль"}).encode("utf-8"))
+ return
+
+ import re
+ if not re.match(r"^[a-zA-Zа-яА-ЯёЁ0-9_-]{3,20}$", username):
+ self.send_response(400)
+ self._set_cors()
+ self.end_headers()
+ self.wfile.write(json.dumps({"success": False, "message": "Логин может содержать только буквы, цифры, '-' и '_' (3-20 симв.)"}).encode("utf-8"))
+ return
+
+ user = db.get_user_by_username(username)
+ if not user:
+ self.send_response(404)
+ self._set_cors()
+ self.end_headers()
+ self.wfile.write(json.dumps({"success": False, "message": "Пользователь с таким логином не найден. Зарегистрируйтесь."}).encode("utf-8"))
+ return
+
+ res = db.authenticate_user(username, password_hash)
+ if not res or not res.get("success"):
+ self.send_response(401)
+ self._set_cors()
+ self.end_headers()
+ self.wfile.write(json.dumps(res or {"success": False, "message": "Неверный логин или пароль"}).encode("utf-8"))
+ return
+
+ data = json.dumps(res, ensure_ascii=False).encode("utf-8")
+ self.send_response(200)
+ self._set_cors()
+ self.send_header("Content-Type", "application/json; charset=utf-8")
+ self.send_header("Content-Length", str(len(data)))
+ self.end_headers()
+ self.wfile.write(data)
+ return
+
+ # 1b. User Registration
+ if path in ["/api/auth/register", "/api/bugs/auth/register"]:
username = str(payload.get("username", "")).strip()
password_hash = str(payload.get("passwordHash", "")).strip()
initial_data = payload.get("initialData")
@@ -174,27 +219,42 @@ class FeedbackHandler(http.server.BaseHTTPRequestHandler):
self.send_response(400)
self._set_cors()
self.end_headers()
- self.wfile.write(json.dumps({"success": False, "message": "Имя пользователя и пароль обязательны"}).encode("utf-8"))
+ self.wfile.write(json.dumps({"success": False, "message": "Введите логин и пароль для регистрации"}).encode("utf-8"))
+ return
+
+ import re
+ if not re.match(r"^[a-zA-Zа-яА-ЯёЁ0-9_-]{3,20}$", username):
+ self.send_response(400)
+ self._set_cors()
+ self.end_headers()
+ self.wfile.write(json.dumps({"success": False, "message": "Логин может содержать только буквы, цифры, '-' и '_' (3-20 симв.)"}).encode("utf-8"))
+ return
+
+ if len(password_hash) < 1:
+ self.send_response(400)
+ self._set_cors()
+ self.end_headers()
+ self.wfile.write(json.dumps({"success": False, "message": "Пароль не может быть пустым"}).encode("utf-8"))
+ return
+
+ if username.lower() in ["гость", "guest", "admin", "null", "undefined", "system", "anonymous"]:
+ self.send_response(400)
+ self._set_cors()
+ self.end_headers()
+ self.wfile.write(json.dumps({"success": False, "message": "Это имя пользователя зарезервировано"}).encode("utf-8"))
return
- # Check if user exists
user = db.get_user_by_username(username)
if user:
- res = db.authenticate_user(username, password_hash)
- if not res or not res.get("success"):
- self.send_response(401)
- self._set_cors()
- self.end_headers()
- self.wfile.write(json.dumps(res or {"success": False, "message": "Неверный пароль"}).encode("utf-8"))
- return
- resp_json = res
- else:
- # Auto-register new user
- res = db.register_user(username, password_hash, initial_data)
- resp_json = res
+ self.send_response(409)
+ self._set_cors()
+ self.end_headers()
+ self.wfile.write(json.dumps({"success": False, "message": "Пользователь с таким логином уже существует"}).encode("utf-8"))
+ return
- data = json.dumps(resp_json, ensure_ascii=False).encode("utf-8")
- self.send_response(200)
+ res = db.register_user(username, password_hash, initial_data)
+ data = json.dumps(res, ensure_ascii=False).encode("utf-8")
+ self.send_response(200 if res.get("success") else 400)
self._set_cors()
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
diff --git a/scripts/test_game.py b/scripts/test_game.py
index 6ef5420..a108118 100644
--- a/scripts/test_game.py
+++ b/scripts/test_game.py
@@ -186,6 +186,7 @@ def test_auth_system():
"getCurrentUser",
"isGuest",
"login",
+ "register",
"loginAsGuest",
"logout",
"getUserData",
@@ -193,6 +194,8 @@ def test_auth_system():
"flushSave",
"scheduleSave",
"apiPost",
+ "validateUsername",
+ "validatePassword"
]
for m in auth_methods:
if m in auth_code:
@@ -205,6 +208,19 @@ def test_auth_system():
record("auth.js has debounced save (scheduleSave)", "PASS" if "scheduleSave" in auth_code else "FAIL")
record("auth.js has flushSave for game-over sync", "PASS" if "flushSave" in auth_code else "FAIL")
+ # Check HTML auth views
+ index_html_path = os.path.join(GAME_DIR, "index.html")
+ with open(index_html_path, "r", encoding="utf-8") as f:
+ html_code = f.read()
+
+ record("Auth Login view present", "PASS" if 'id="auth-login-view"' in html_code else "FAIL")
+ record("Auth Register view present", "PASS" if 'id="auth-register-view"' in html_code else "FAIL")
+ record("Auth Logged-in view present", "PASS" if 'id="auth-logged-in-view"' in html_code else "FAIL")
+ record("Auth switch to register button present", "PASS" if 'id="btn-auth-to-register"' in html_code else "FAIL")
+ record("Auth do register button present", "PASS" if 'id="btn-auth-do-register"' in html_code else "FAIL")
+ record("Auth guest button present", "PASS" if 'id="btn-auth-guest"' in html_code else "FAIL")
+ record("Auth logout button present", "PASS" if 'id="btn-auth-logout"' in html_code else "FAIL")
+
# Server-side SQLite DB tests
print("\n--- 4b. SQLite DB & Auth API CRUD Tests ---")
sys.path.insert(0, os.path.join(BASE_DIR, "scripts"))
@@ -233,6 +249,10 @@ def test_auth_system():
record("SQLite DB: reject wrong password", "PASS" if bad_auth and not bad_auth.get("success") else "FAIL",
"Bad password was accepted!" if bad_auth and bad_auth.get("success") else "")
+ # Test duplicate registration rejection
+ dup_reg = pvz_db.register_user("kopri", "c63bc483")
+ record("SQLite DB: reject duplicate user registration", "PASS" if not dup_reg.get("success") else "FAIL")
+
# Test save profile (update newbie coins)
save_res = pvz_db.save_user_profile("newbie_test", {"coins": 999, "level": 3, "highScore": 2500})
record("SQLite DB: save_user_profile() succeeds", "PASS" if save_res.get("success") else "FAIL")