## Features Added ### Admin Login Page (public/login.html) - Professional login UI with Bootstrap 5 - Email/password authentication - CSRF protection - Rate limiting protection - Session persistence (7 days) - Remember me functionality - Language: Spanish with translations ready ### Authentication Middleware (src/server/index.ts) - Session-based authentication using SQLite - bcrypt password hashing - CSRF token endpoint for form protection - Auth check on admin.html page load - Logout endpoint ### API Client Enhancements (public/js/api.js) - Added auth methods: login(), logout(), getMe(), getCsrfToken() - CRUD methods for all admin entities: - Properties: create, update, delete - Leads: get, update, delete - Testimonials: create, update, delete - FAQ: create, update, delete - Services: create, update, delete - Settings: get, update - Admin stats endpoint ### Comprehensive Seed Data (src/db/seed-comprehensive.ts) - 36 properties of all types: - 8 urban lands - 10 agricultural plots - 8 houses/villas - 10 apartments - Real Tenerife locations with coordinates - Spanish and Russian translations - 8 testimonials from international clients - 8 FAQ items (buying process, taxes, etc.) - 6 services offered - Admin user: admin@tenerifeprop.com / admin123 - Stock photos from Unsplash ### Tests (tests/auth.test.ts) - Authentication tests - Session management tests - Property CRUD tests - Input validation tests - XSS prevention tests - Email/phone validation tests ## Why These Changes 1. Security: Authentication protects admin routes from unauthorized access 2. Data: Seed data provides realistic content for testing and demo 3. UX: Professional login page improves user experience 4. Testing: Tests ensure reliability and catch regressions ## Breaking Changes None - all changes are additive ## Related Issues - Closes #28 (Admin Login Page) - Closes #29 (Seed Data Generation) - Closes #30 (Tests Implementation) ## Milestone Administrative Section Implementation (#51)
288 lines
7.9 KiB
JavaScript
288 lines
7.9 KiB
JavaScript
// TenerifeProp - API Client
|
|
const API_BASE = '/api';
|
|
|
|
class API {
|
|
// Auth
|
|
static async login(email, password) {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/auth/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email, password })
|
|
});
|
|
return response.json();
|
|
} catch (error) {
|
|
return { success: false, error: 'Error de conexión' };
|
|
}
|
|
}
|
|
|
|
static async logout() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/auth/logout`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
return response.json();
|
|
} catch (error) {
|
|
return { success: false, error: 'Error de conexión' };
|
|
}
|
|
}
|
|
|
|
static async getMe() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/auth/me`);
|
|
return response.json();
|
|
} catch (error) {
|
|
return { success: false, error: 'Error de conexión' };
|
|
}
|
|
}
|
|
|
|
static async getCsrfToken() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/csrf-token`);
|
|
return response.json();
|
|
} catch (error) {
|
|
return { success: false, error: 'Error de conexión' };
|
|
}
|
|
}
|
|
|
|
// Properties
|
|
static async getProperties(filters = {}) {
|
|
const params = new URLSearchParams(filters);
|
|
const response = await fetch(`${API_BASE}/properties?${params}`);
|
|
return response.json();
|
|
}
|
|
|
|
static async getProperty(slug, lang = 'es') {
|
|
const response = await fetch(`${API_BASE}/properties/${slug}?lang=${lang}`);
|
|
return response.json();
|
|
}
|
|
|
|
static async getPropertyBySlug(slug, lang = 'es') {
|
|
const response = await fetch(`${API_BASE}/properties/${slug}?lang=${lang}`);
|
|
return response.json();
|
|
}
|
|
|
|
static async getFeaturedProperties(lang = 'es') {
|
|
const response = await fetch(`${API_BASE}/properties/featured?lang=${lang}`);
|
|
return response.json();
|
|
}
|
|
|
|
// Admin Properties
|
|
static async createProperty(data) {
|
|
const response = await fetch(`${API_BASE}/admin/properties`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
return response.json();
|
|
}
|
|
|
|
static async updateProperty(id, data) {
|
|
const response = await fetch(`${API_BASE}/admin/properties/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
return response.json();
|
|
}
|
|
|
|
static async deleteProperty(id) {
|
|
const response = await fetch(`${API_BASE}/admin/properties/${id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
return response.json();
|
|
}
|
|
|
|
// Leads
|
|
static async createLead(data) {
|
|
// Input validation
|
|
if (!data.name || typeof data.name !== 'string' || data.name.trim().length < 2) {
|
|
return { success: false, error: 'Name is required (min 2 characters)' }
|
|
}
|
|
// Proper email validation
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
if (!data.email || typeof data.email !== 'string' || !emailRegex.test(data.email)) {
|
|
return { success: false, error: 'Valid email is required' }
|
|
}
|
|
// Sanitize inputs to prevent XSS
|
|
const sanitize = (str) => str ? String(str).replace(/[<>]/g, '') : ''
|
|
const sanitizedData = {
|
|
name: sanitize(data.name),
|
|
email: data.email,
|
|
phone: sanitize(data.phone),
|
|
message: sanitize(data.message),
|
|
property_id: data.property_id,
|
|
language: data.language
|
|
}
|
|
|
|
const response = await fetch(`${API_BASE}/leads`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(sanitizedData)
|
|
});
|
|
return response.json();
|
|
}
|
|
|
|
static async getLeads(filters = {}) {
|
|
const params = new URLSearchParams(filters);
|
|
const response = await fetch(`${API_BASE}/admin/leads?${params}`);
|
|
return response.json();
|
|
}
|
|
|
|
static async updateLead(id, data) {
|
|
const response = await fetch(`${API_BASE}/admin/leads/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
return response.json();
|
|
}
|
|
|
|
static async deleteLead(id) {
|
|
const response = await fetch(`${API_BASE}/admin/leads/${id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
return response.json();
|
|
}
|
|
|
|
// Content
|
|
static async getTestimonials(lang = 'es') {
|
|
const response = await fetch(`${API_BASE}/testimonials?lang=${lang}`);
|
|
return response.json();
|
|
}
|
|
|
|
static async createTestimonial(data) {
|
|
const response = await fetch(`${API_BASE}/admin/testimonials`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
return response.json();
|
|
}
|
|
|
|
static async updateTestimonial(id, data) {
|
|
const response = await fetch(`${API_BASE}/admin/testimonials/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
return response.json();
|
|
}
|
|
|
|
static async deleteTestimonial(id) {
|
|
const response = await fetch(`${API_BASE}/admin/testimonials/${id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
return response.json();
|
|
}
|
|
|
|
static async getFAQ(lang = 'es') {
|
|
const response = await fetch(`${API_BASE}/faq?lang=${lang}`);
|
|
return response.json();
|
|
}
|
|
|
|
static async createFAQ(data) {
|
|
const response = await fetch(`${API_BASE}/admin/faq`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
return response.json();
|
|
}
|
|
|
|
static async updateFAQ(id, data) {
|
|
const response = await fetch(`${API_BASE}/admin/faq/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
return response.json();
|
|
}
|
|
|
|
static async deleteFAQ(id) {
|
|
const response = await fetch(`${API_BASE}/admin/faq/${id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
return response.json();
|
|
}
|
|
|
|
static async getServices(lang = 'es') {
|
|
const response = await fetch(`${API_BASE}/services?lang=${lang}`);
|
|
return response.json();
|
|
}
|
|
|
|
static async createService(data) {
|
|
const response = await fetch(`${API_BASE}/admin/services`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
return response.json();
|
|
}
|
|
|
|
static async updateService(id, data) {
|
|
const response = await fetch(`${API_BASE}/admin/services/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
return response.json();
|
|
}
|
|
|
|
static async deleteService(id) {
|
|
const response = await fetch(`${API_BASE}/admin/services/${id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
return response.json();
|
|
}
|
|
|
|
static async getSettings() {
|
|
const response = await fetch(`${API_BASE}/settings`);
|
|
return response.json();
|
|
}
|
|
|
|
static async updateSettings(data) {
|
|
const response = await fetch(`${API_BASE}/admin/settings`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
return response.json();
|
|
}
|
|
|
|
// Admin Stats
|
|
static async getAdminStats() {
|
|
const response = await fetch(`${API_BASE}/admin/stats`);
|
|
return response.json();
|
|
}
|
|
|
|
// Analytics
|
|
static async trackEvent(type, data = {}) {
|
|
let sessionId = localStorage.getItem('session_id');
|
|
if (!sessionId) {
|
|
// Fallback for non-secure contexts (HTTP)
|
|
sessionId = crypto.randomUUID ? crypto.randomUUID() : 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
localStorage.setItem('session_id', sessionId);
|
|
}
|
|
|
|
await fetch(`${API_BASE}/analytics/event`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
type,
|
|
session_id: sessionId,
|
|
...data
|
|
})
|
|
});
|
|
}
|
|
|
|
// Cities
|
|
static async getCities() {
|
|
const response = await fetch(`${API_BASE}/cities`);
|
|
return response.json();
|
|
}
|
|
}
|
|
|
|
// Export for use
|
|
window.API = API; |