Files
TenerifeProp/public/js/api.js
TenerifeProp Dev e6ea1400d6 feat: implement administrative section with authentication and seed data
## 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)
2026-04-06 00:21:34 +01:00

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;