Fix login, root route, hero redirect and game page API handling #1

Merged
bepis merged 1 commits from fix/windows-playable into main 2026-10-21 13:55:05 +11:00
4 changed files with 44 additions and 32 deletions
+33 -29
View File
@@ -19,6 +19,10 @@ function switchTab(tab) {
document.getElementById('hunt-tab').style.display = tab === 'hunt' ? '' : 'none'; document.getElementById('hunt-tab').style.display = tab === 'hunt' ? '' : 'none';
document.getElementById('expedition-tab').style.display = tab === 'expedition' ? '' : 'none'; document.getElementById('expedition-tab').style.display = tab === 'expedition' ? '' : 'none';
document.getElementById('inventory-tab').style.display = tab === 'inventory' ? '' : 'none'; document.getElementById('inventory-tab').style.display = tab === 'inventory' ? '' : 'none';
if (tab === 'expedition') {
loadExpeditions();
startExpeditionSync();
}
} }
/** /**
@@ -136,6 +140,7 @@ async function loadCharacter() {
} }
const { character, derived, energy } = data; const { character, derived, energy } = data;
window.heroLevel = character.level;
// Show stats, hide loading // Show stats, hide loading
document.getElementById('hero-loading').style.display = 'none'; document.getElementById('hero-loading').style.display = 'none';
@@ -177,7 +182,7 @@ async function loadZones() {
const zoneList = document.getElementById('zone-list'); const zoneList = document.getElementById('zone-list');
try { try {
const data = await api.get('/zones'); const data = await api.get('/zones');
if (!data || !data.ok) { if (!Array.isArray(data)) {
zoneList.innerHTML = ''; zoneList.innerHTML = '';
const p = document.createElement('p'); const p = document.createElement('p');
p.className = 'placeholder'; p.className = 'placeholder';
@@ -186,7 +191,7 @@ async function loadZones() {
return; return;
} }
const zones = data.data; const zones = data;
zoneList.innerHTML = ''; zoneList.innerHTML = '';
for (const zone of zones) { for (const zone of zones) {
@@ -256,18 +261,18 @@ async function handleHunt(e) {
try { try {
const data = await api.post('/hunt', { zoneId }); const data = await api.post('/hunt', { zoneId });
if (!data || !data.ok) { if (!data) {
showToast(data?.error?.message || 'Hunt failed.', 'error'); showToast(data?.error?.message || 'Hunt failed.', 'error');
return; return;
} }
const result = data.data; const result = data;
displayHuntResult(result); displayHuntResult(result);
loadCharacter(); loadCharacter();
loadCombatLog(); loadCombatLog();
loadZones(); // refresh energy cost buttons loadZones(); // refresh energy cost buttons
} catch (err) { } catch (err) {
showToast('Hunt failed.', 'error'); showToast(err.message || 'Hunt failed.', 'error');
} finally { } finally {
isHunting = false; isHunting = false;
// Re-enable all hunt buttons // Re-enable all hunt buttons
@@ -352,7 +357,7 @@ async function loadCombatLog() {
const logList = document.getElementById('combat-log-list'); const logList = document.getElementById('combat-log-list');
try { try {
const data = await api.get('/combat-log?limit=10'); const data = await api.get('/combat-log?limit=10');
if (!data || !data.ok) { if (!Array.isArray(data)) {
logList.innerHTML = ''; logList.innerHTML = '';
const p = document.createElement('p'); const p = document.createElement('p');
p.className = 'placeholder'; p.className = 'placeholder';
@@ -361,7 +366,7 @@ async function loadCombatLog() {
return; return;
} }
const logs = data.data; const logs = data;
logList.innerHTML = ''; logList.innerHTML = '';
if (logs.length === 0) { if (logs.length === 0) {
@@ -595,8 +600,8 @@ async function handleItemClick(item) {
} else { } else {
data = await api.post('/inventory/equip', { itemId: item.itemId, slot: item.type }); data = await api.post('/inventory/equip', { itemId: item.itemId, slot: item.type });
} }
if (!data || !data.ok) { if (!data) {
showToast(data?.error?.message || 'Action failed.', 'error'); showToast('Action failed.', 'error');
return; return;
} }
inventoryCache = data; inventoryCache = data;
@@ -611,8 +616,8 @@ async function handleItemClick(item) {
async function handleUnequip(slot) { async function handleUnequip(slot) {
try { try {
const data = await api.post('/inventory/unequip', { slot }); const data = await api.post('/inventory/unequip', { slot });
if (!data || !data.ok) { if (!data) {
showToast(data?.error?.message || 'Unequip failed.', 'error'); showToast('Unequip failed.', 'error');
return; return;
} }
inventoryCache = data; inventoryCache = data;
@@ -649,14 +654,13 @@ function formatDuration(sec) {
async function loadExpeditions() { async function loadExpeditions() {
try { try {
const data = await api.get('/expeditions/active'); const data = await api.get('/expeditions/active'); // null when no active expedition
if (!data || !data.ok) return;
const statusEl = document.getElementById('expedition-status'); const statusEl = document.getElementById('expedition-status');
const defsEl = document.getElementById('expedition-defs'); const defsEl = document.getElementById('expedition-defs');
if (data.data) { if (data) {
renderActiveExpedition(statusEl, defsEl, data.data); renderActiveExpedition(statusEl, defsEl, data);
} else { } else {
statusEl.innerHTML = ''; statusEl.innerHTML = '';
renderExpeditionDefs(defsEl); renderExpeditionDefs(defsEl);
@@ -739,9 +743,9 @@ function renderExpeditionDefs(container) {
// Fetch expedition definitions from the server // Fetch expedition definitions from the server
api.get('/expeditions/defs').then(defsData => { api.get('/expeditions/defs').then(defsData => {
if (!defsData || !defsData.ok || !defsData.data) return; if (!Array.isArray(defsData)) return;
const defs = defsData.data; const defs = defsData;
for (const def of defs) { for (const def of defs) {
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'expedition-def-card'; card.className = 'expedition-def-card';
@@ -756,20 +760,20 @@ function renderExpeditionDefs(container) {
const infoEl = document.createElement('div'); const infoEl = document.createElement('div');
infoEl.className = 'expedition-info'; infoEl.className = 'expedition-info';
infoEl.textContent = `${formatDuration(def.durationSec)} · ${def.energyCost}⚡ · Lv${def.min_level}+`; infoEl.textContent = `${formatDuration(def.durationSec)} · ${def.energyCost}⚡ · Lv${def.minLevel}+`;
const rewardsEl = document.createElement('div'); const rewardsEl = document.createElement('div');
rewardsEl.className = 'expedition-rewards'; rewardsEl.className = 'expedition-rewards';
rewardsEl.textContent = `+${def.xpReward} XP, ${def.goldMin}–${def.gold_max} Gold`; rewardsEl.textContent = `+${def.xpReward} XP, ${def.goldMin}–${def.goldMax} Gold`;
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.className = 'btn btn-expedition'; btn.className = 'btn btn-expedition';
btn.textContent = `Start Expedition (${def.energyCost}⚡)`; btn.textContent = `Start Expedition (${def.energyCost}⚡)`;
btn.disabled = window.heroLevel < def.min_level; btn.disabled = window.heroLevel < def.minLevel;
btn.dataset.defId = def.id; btn.dataset.defId = def.id;
if (window.heroLevel < def.min_level) { if (window.heroLevel < def.minLevel) {
btn.title = `Requires level ${def.min_level}`; btn.title = `Requires level ${def.minLevel}`;
} }
card.appendChild(nameEl); card.appendChild(nameEl);
@@ -796,15 +800,15 @@ async function handleStartExpedition(e) {
try { try {
const data = await api.post('/expeditions/start', { defId }); const data = await api.post('/expeditions/start', { defId });
if (!data || !data.ok) { if (!data) {
showToast(data?.error?.message || 'Failed to start expedition.', 'error'); showToast('Failed to start expedition.', 'error');
return; return;
} }
showToast('Expedition started!', 'info'); showToast('Expedition started!', 'info');
loadExpeditions(); loadExpeditions();
loadCharacter(); // refresh energy loadCharacter(); // refresh energy
} catch (err) { } catch (err) {
showToast('Failed to start expedition.', 'error'); showToast(err.message || 'Failed to start expedition.', 'error');
} }
} }
@@ -818,17 +822,17 @@ async function handleClaimExpedition(e) {
try { try {
const data = await api.post('/expeditions/claim', { expeditionId }); const data = await api.post('/expeditions/claim', { expeditionId });
if (!data || !data.ok) { if (!data) {
showToast(data?.error?.message || 'Failed to claim expedition.', 'error'); showToast('Failed to claim expedition.', 'error');
return; return;
} }
const { xpGained, goldGained } = data.data; const { xpGained, goldGained } = data;
showToast(`Expedition complete! +${xpGained} XP, +${goldGained} Gold`, 'info'); showToast(`Expedition complete! +${xpGained} XP, +${goldGained} Gold`, 'info');
loadExpeditions(); loadExpeditions();
loadCharacter(); // refresh gold/XP loadCharacter(); // refresh gold/XP
} catch (err) { } catch (err) {
showToast('Failed to claim expedition.', 'error'); showToast(err.message || 'Failed to claim expedition.', 'error');
} }
} }
+1 -1
View File
@@ -15,7 +15,7 @@
<label for="username">Username</label> <label for="username">Username</label>
<input type="text" id="username" name="username" required minlength="3" maxlength="20" autocomplete="username"> <input type="text" id="username" name="username" required minlength="3" maxlength="20" autocomplete="username">
<label for="password">Password</label> <label for="password">Password</label>
<input type="password" id="password" name="password" required minlength="8" maxlength="72" autocomplete="current-password"> <input type="password" id="password" name="password" required maxlength="72" autocomplete="current-password">
<button type="submit">Login</button> <button type="submit">Login</button>
</form> </form>
<p class="redirect">Don't have an account? <a href="/register.html">Register</a></p> <p class="redirect">Don't have an account? <a href="/register.html">Register</a></p>
+3
View File
@@ -45,6 +45,9 @@ function createApp() {
res.json({ ok: true }); res.json({ ok: true });
}); });
// Root: send visitors to the login page (there is no index.html)
app.get('/', (_req, res) => res.redirect('/login.html'));
// Auth routes (public) // Auth routes (public)
app.post('/api/auth/register', registerLimiter, authRoutes.postRegister); app.post('/api/auth/register', registerLimiter, authRoutes.postRegister);
app.post('/api/auth/login', loginLimiter, authRoutes.postLogin); app.post('/api/auth/login', loginLimiter, authRoutes.postLogin);
+7 -2
View File
@@ -2,6 +2,11 @@ const rateLimit = require('express-rate-limit');
const zod = require('zod'); const zod = require('zod');
const { AppError } = require('../utils/errors'); const { AppError } = require('../utils/errors');
const { register, verifyLogin } = require('../services/authService'); const { register, verifyLogin } = require('../services/authService');
const { getDb } = require('../db');
function userHasHero(userId) {
return !!getDb().prepare('SELECT 1 FROM characters WHERE user_id = ?').get(userId);
}
// Validation schemas // Validation schemas
const registerSchema = zod.strictObject({ const registerSchema = zod.strictObject({
@@ -63,7 +68,7 @@ async function handleLogin(req, res) {
req.session.regenerate((err) => { req.session.regenerate((err) => {
if (err) throw new AppError('INTERNAL_ERROR', 'Session regeneration failed.', 500); if (err) throw new AppError('INTERNAL_ERROR', 'Session regeneration failed.', 500);
req.session.userId = user.id; req.session.userId = user.id;
res.json({ ok: true, data: { user: { id: user.id, username: user.username }, hasHero: false } }); res.json({ ok: true, data: { user: { id: user.id, username: user.username }, hasHero: userHasHero(user.id) } });
}); });
} }
@@ -77,7 +82,7 @@ async function handleLogout(req, res) {
async function handleMe(req, res) { async function handleMe(req, res) {
const userId = req.session.userId; const userId = req.session.userId;
res.json({ ok: true, data: { user: { id: userId, username: req.session.username || '' }, hasHero: false } }); res.json({ ok: true, data: { user: { id: userId, username: req.session.username || '' }, hasHero: userHasHero(userId) } });
} }
module.exports = { module.exports = {