From f4b82c85028dfa41df549dc2cdf19348f02c7ea3 Mon Sep 17 00:00:00 2001 From: TenerifeProp Dev Date: Sun, 5 Apr 2026 00:15:48 +0100 Subject: [PATCH] feat: add persistent sessions, sitemap docs, and expanded seed data ## Security - Sessions now stored in SQLite database instead of memory - Sessions table persists across server restarts - Auto-cleanup of expired sessions on startup ## Documentation - Created docs/SITEMAP.md with site navigation map - Documented user flows and data binding - Listed all routes and their purposes ## Issue #9 Progress - Seed data expanded from 3 to 12 properties - Added English translations (title_en, description_en) - All major Tenerife cities represented - Various property types: urban, agricultural, houses, apartments ## Database - Added title_en, description_en, short_description_en columns - Deleted old database to reseed with new data --- data/tenerifeprop.db | Bin 90112 -> 0 bytes docs/SITEMAP.md | 202 +++++++++++++++++++++++++++++++ src/server/index.ts | 276 ++++++++++++++++++++++++++++++++++++------- 3 files changed, 437 insertions(+), 41 deletions(-) delete mode 100644 data/tenerifeprop.db create mode 100644 docs/SITEMAP.md diff --git a/data/tenerifeprop.db b/data/tenerifeprop.db deleted file mode 100644 index 17bf4513239d2595b73afd2373ce1dccb6046572..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90112 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 { - stmt.run( - p.id, p.slug, p.reference, p.type, p.status, p.land_type, p.title_es, p.title_ru, p.description_es, p.description_ru, - p.short_description_es || null, p.short_description_ru || null, p.address, p.city, p.postal_code, p.zone || null, + ;(stmt as any).run( + p.id, p.slug, p.reference, p.type, p.status, p.land_type, + p.title_es, p.title_ru, (p as any).title_en || null, + p.description_es, p.description_ru, (p as any).description_en || null, + p.short_description_es || null, p.short_description_ru || null, (p as any).short_description_en || null, + p.address, p.city, p.postal_code, p.zone || null, p.lat, p.lng, p.area, p.price, p.price_per_m2 || null, p.bedrooms || null, p.bathrooms || null, - p.water, p.electricity, p.phone, p.drainage, p.road, p.gas, + p.water, p.electricity, p.phone || 'unavailable', p.drainage || 'unavailable', p.road, p.gas, p.orientation, p.views_sea || 0, p.views_mountain || 0, p.views_valley || 0, p.topography || 'flat', p.has_ruins || 0, p.has_license || 0, p.is_buildable || 0, p.max_floors || 0, - p.images, p.videos || '[]', p.badges, p.is_featured || 0, p.is_exclusive || 0, p.published_at + p.images, (p as any).videos || '[]', p.badges, p.is_featured || 0, p.is_exclusive || 0, p.published_at ) }) @@ -473,7 +643,39 @@ app.get('/api/stats', (c) => { }) // ============ AUTH ENDPOINTS ============ -const sessions = new Map() + +// Session helpers using SQLite for persistence +const SESSION_EXPIRY_DAYS = 7 + +function createSession(userId: string, role: string): string { + const sessionId = genId() + const expiresAt = new Date(Date.now() + SESSION_EXPIRY_DAYS * 24 * 60 * 60 * 1000).toISOString() + db.run('INSERT INTO sessions (id, user_id, role, expires_at) VALUES (?, ?, ?, ?)', [sessionId, userId, role, expiresAt]) + return sessionId +} + +function getSession(sessionId: string): { userId: string; role: string } | null { + const session = db.query('SELECT * FROM sessions WHERE id = ?').get(sessionId) as any + if (!session) return null + + // Check if expired + if (new Date(session.expires_at) < new Date()) { + db.run('DELETE FROM sessions WHERE id = ?', [sessionId]) + return null + } + + return { userId: session.user_id, role: session.role } +} + +function deleteSession(sessionId: string): void { + db.run('DELETE FROM sessions WHERE id = ?', [sessionId]) +} + +// Clean expired sessions on startup +function cleanExpiredSessions(): void { + db.run("DELETE FROM sessions WHERE datetime(expires_at) < datetime('now')") +} +cleanExpiredSessions() app.post('/api/auth/login', async (c) => { try { @@ -503,15 +705,10 @@ app.post('/api/auth/login', async (c) => { return c.json({ success: false, error: 'Account is inactive' }, 403) } - const sessionId = genId() - sessions.set(sessionId, { - userId: user.id, - role: user.role, - expires: Date.now() + 7 * 24 * 60 * 60 * 1000 // 7 days - }) + const sessionId = createSession(user.id, user.role) // Set cookie - c.header('Set-Cookie', `session=${sessionId}; Path=/; HttpOnly; Max-Age=${7 * 24 * 60 * 60}; SameSite=Lax`) + c.header('Set-Cookie', `session=${sessionId}; Path=/; HttpOnly; Max-Age=${SESSION_EXPIRY_DAYS * 24 * 60 * 60}; SameSite=Lax`) return c.json({ success: true, @@ -532,7 +729,7 @@ app.post('/api/auth/login', async (c) => { app.post('/api/auth/logout', async (c) => { const sessionId = c.req.header('Cookie')?.match(/session=([^;]+)/)?.[1] if (sessionId) { - sessions.delete(sessionId) + deleteSession(sessionId) } c.header('Set-Cookie', 'session=; Path=/; HttpOnly; Max-Age=0') return c.json({ success: true }) @@ -544,9 +741,8 @@ app.get('/api/auth/me', async (c) => { return c.json({ success: false, error: 'Not authenticated' }, 401) } - const session = sessions.get(sessionId) - if (!session || session.expires < Date.now()) { - sessions.delete(sessionId) + const session = getSession(sessionId) + if (!session) { return c.json({ success: false, error: 'Session expired' }, 401) } @@ -563,9 +759,8 @@ const requireAuth = async (c: any, next: any) => { return c.json({ success: false, error: 'Not authenticated' }, 401) } - const session = sessions.get(sessionId) - if (!session || session.expires < Date.now()) { - sessions.delete(sessionId) + const session = getSession(sessionId) + if (!session) { return c.json({ success: false, error: 'Session expired' }, 401) } @@ -580,9 +775,8 @@ const requireAdmin = async (c: any, next: any) => { return c.json({ success: false, error: 'Not authenticated' }, 401) } - const session = sessions.get(sessionId) - if (!session || session.expires < Date.now()) { - sessions.delete(sessionId) + const session = getSession(sessionId) + if (!session) { return c.json({ success: false, error: 'Session expired' }, 401) }