diff --git a/data/tenerifeprop.db b/data/tenerifeprop.db deleted file mode 100644 index 17bf451..0000000 Binary files a/data/tenerifeprop.db and /dev/null differ diff --git a/docs/SITEMAP.md b/docs/SITEMAP.md new file mode 100644 index 0000000..1dadae8 --- /dev/null +++ b/docs/SITEMAP.md @@ -0,0 +1,202 @@ +# TenerifeProp - Карта сайта + +## Структура страниц + +### Публичные страницы + +``` +/ # Главная страница (Landing) +├── #hero # Главный баннер +├── #advantages # Преимущества +├── #catalog # Каталог недвижимости +├── #services # Услуги +├── #testimonials # Отзывы +├── #faq # Вопросы-ответы +└── #contact # Контакты + +/property/:slug # Страница объекта недвижимости +├── #gallery # Галерея изображений +├── #details # Детали объекта +├── #utilities # Коммуникации +├── #location # Карта +└── #contact-form # Форма запроса + +/admin # Админ-панель (требует авторизации) +├── /dashboard # Dashboard - статистика +├── /properties # Управление объектами +│ ├── /list # Список +│ └── /new # Новый объект +├── /leads # Управление заявками +├── /testimonials # Управление отзывами +├── /faq # Управление FAQ +├── /services # Управление услугами +└── /settings # Настройки сайта +``` + +## Навигация + +### Главное меню (Navbar) +- **Inicio** → `/#hero` +- **Catálogo** → `/#catalog` +- **Servicios** → `/#services` +- **Testimonios** → `/#testimonials` +- **Contacto** → `/#contact` + +### Языковой переключатель +- **ES** (Испанский) - по умолчанию +- **RU** (Русский) +- **EN** (Английский) + +### Footer меню +- **Aviso Legal** → `/legal` +- **Política de Privacidad** → `/privacy` +- **Política de Cookies** → `/cookies` + +## Пользовательский воркфлоу + +### Покупатель (Client Journey) + +``` +1. Главная страница + ↓ +2. Просмотр каталога → Фильтры (тип, цена, город) + ↓ +3. Выбор объекта → Карточка объекта + ↓ +4. Просмотр деталий (фото, характеристики, карта) + ↓ +5. Отправка заявки (форма контакта) + ↓ +6. Получение подтверждения → Ожидание звонка +``` + +### Администратор (Admin Journey) + +``` +1. Вход в админку → /admin + ↓ +2. Dashboard → Статистика, графики + ↓ +3. Управление объектами + ├── Добавление нового объекта + ├── Редактирование объекта + └── Изменение статуса (active/sold/reserved) + ↓ +4. Управление заявками + ├── Просмотр новых заявок + ├── Изменение статуса (new/contaced/closed) + └── Добавление заметок + ↓ +5. Управление контентом + ├── Отзывы → Модерация + ├── FAQ → Редактирование + └── Услуги → Обновление +``` + +## Связность данных (Data Binding) + +### Связи между сущностями + +``` +Property +├── id (UUID) +├── slug (URL-friendly) +├── city → Фильтр по городам +├── type → Фильтр по типу +└── leads → Заявки с property_id + +Lead +├── id (UUID) +├── property_id → Связь с Property +└── status → Статус обработки + +Testimonial +├── id (UUID) +├── property_id → Связь с Property (опционально) +└── is_approved → Модерация + +FAQ +├── id (UUID) +├── category → Группировка +└── order_num → Порядок отображения + +Service +├── id (UUID) +├── order_num → Порядок отображения +└── is_active → Активность +``` + +## API Endpoints связности + +### Получение объекта по slug +```http +GET /api/properties/:slug?lang=es +``` + +### Получение похожих объектов +```http +GET /api/properties?type=urban&city=Adeje&limit=3 +``` + +### Создание заявки для объекта +```http +POST /api/leads +{ + "name": "John Doe", + "email": "john@example.com", + "phone": "+34 600 123 456", + "property_id": "prop-001", + "message": "Interested in this property" +} +``` + +## Роутинг + +### Public Routes +| Route | File | Description | +|-------|------|-------------| +| `/` | `public/index.html` | Landing page | +| `/property/:slug` | `public/property.html` | Property details | +| `/admin` | `public/admin.html` | Admin panel SPA | +| `/admin/*` | `public/admin.html` | Admin SPA routes | + +### API Routes +| Prefix | Access | Description | +|--------|--------|-------------| +| `/api/auth/*` | Public | Authentication | +| `/api/properties/*` | Public | Properties catalog | +| `/api/leads` | Public POST only | Create lead | +| `/api/testimonials` | Public | Testimonials | +| `/api/faq` | Public | FAQ | +| `/api/services` | Public | Services | +| `/api/settings` | Public | Site settings | +| `/api/admin/*` | Admin only | Admin operations | + +## Статусы и переходы + +### Property Status +``` +active → reserved → sold + ↓ ↓ +inactive inactive +``` + +### Lead Status +``` +new → contacted → qualified → negotiating → closed + ↓ ↓ +lost lost +``` + +## Кэширование + +### Static Files +- HTML: no-cache (SPA) +- CSS/JS: cache with version +- Images: long-term cache + +### API Responses +- Properties: 5 min cache +- Settings: 1 hour cache +- FAQ/Services: 10 min cache +- Leads: no cache \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 7b8de7d..ea1f437 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -19,10 +19,13 @@ db.run(` land_type TEXT NOT NULL, title_es TEXT NOT NULL, title_ru TEXT NOT NULL, + title_en TEXT, description_es TEXT NOT NULL, description_ru TEXT NOT NULL, + description_en TEXT, short_description_es TEXT, short_description_ru TEXT, + short_description_en TEXT, address TEXT NOT NULL, city TEXT NOT NULL, province TEXT NOT NULL DEFAULT 'Santa Cruz de Tenerife', @@ -155,6 +158,17 @@ db.run(` ) `) +db.run(` + CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + role TEXT NOT NULL, + expires_at TEXT NOT NULL, + created_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (user_id) REFERENCES users(id) + ) +`) + // Middleware app.use('*', cors()) app.use('*', logger()) @@ -186,71 +200,227 @@ function seedData() { const now = new Date().toISOString() - // Properties + // Properties - Comprehensive seed data const props = [ + // URBAN LAND { id: 'prop-001', slug: 'terreno-urbano-adeje', reference: 'TP-001', type: 'urban', status: 'active', land_type: 'urban', title_es: 'Terreno Urbano en Adeje', title_ru: 'Городской участок в Адехе', - description_es: 'Increíble oportunidad de adquirir este terreno urbano de 2.500 m² en Adeje.', - description_ru: 'Потрясающая возможность приобрести этот городской участок площадью 2500 м².', + title_en: 'Urban Land in Adeje', + description_es: 'Increíble oportunidad de adquirir este terreno urbano de 2.500 m² en Adeje, con licencia de obras aprobada para construir una vivienda unifamiliar. El terreno cuenta con todas las conexiones de servicios: agua, luz, teléfono y saneamiento.', + description_ru: 'Потрясающая возможность приобрести этот городской участок площадью 2500 м² в Адехе с утвержденной лицензией на строительство частного дома. Участок имеет все коммуникации: вода, электричество, телефон и канализация.', + description_en: 'Incredible opportunity to acquire this urban land of 2,500 m² in Adeje, with approved building license for a single-family home. The land has all service connections: water, electricity, telephone and sanitation.', short_description_es: 'Terreno urbano de 2.500 m² con licencia de obras', short_description_ru: 'Городской участок 2500 м² с лицензией', + short_description_en: 'Urban land 2,500 m² with building license', address: 'Avda. de la Constitución', city: 'Adeje', postal_code: '38670', zone: 'Costa Adeje', lat: 28.1227, lng: -16.6942, area: 2500, price: 385000, price_per_m2: 154, water: 'available', electricity: 'available', phone: 'available', drainage: 'available', road: 'asphalt', gas: 'planned', - orientation: 'south', views_sea: 1, has_license: 1, is_buildable: 1, max_floors: 2, - images: '["https://images.unsplash.com/photo-1564013799919-ab600027ffc6?w=1920&q=80"]', - badges: '["exclusive", "featured"]', is_featured: 1, is_exclusive: 1, published_at: now + orientation: 'south', views_sea: 1, views_mountain: 1, has_license: 1, is_buildable: 1, max_floors: 2, + images: '["https://images.unsplash.com/photo-1564013799919-ab600027ffc6?w=1920&q=80","https://images.unsplash.com/photo-1500382017468-9049fed747ef?w=800&q=80"]', + badges: '["exclusive","featured"]', is_featured: 1, is_exclusive: 1, published_at: now }, { id: 'prop-002', slug: 'terreno-agricola-guimar', reference: 'TP-002', type: 'agricultural', status: 'active', land_type: 'agricultural', - title_es: 'Terreno Agrícola en Güímar', title_ru: 'Сельскохозяйственный участок в Гуимар', - description_es: 'Hermoso terreno agrícola de 5.000 m² ubicado en Güímar.', - description_ru: 'Прекрасный сельскохозяйственный участок площадью 5000 м².', + title_es: 'Terreno Agrícola en Güímar', title_ru: 'Сельскохозяйственный участок в Гуимар', title_en: 'Agricultural Land in Güímar', + description_es: 'Hermoso terreno agrícola de 5.000 m² ubicado en el Valle de Güímar, con ruinas tradicionales canarias y agua propia. Ideal para cultivo de frutas tropicales o como finca de recreo.', + description_ru: 'Прекрасный сельскохозяйственный участок площадью 5000 м² в долине Гуимар с традиционными канарскими руинами и собственной водой. Идеально подходит для выращивания тропических фруктов или как загородная усадьба.', + description_en: 'Beautiful agricultural land of 5,000 m² located in the Valley of Güímar, with traditional Canarian ruins and own water supply. Ideal for growing tropical fruits or as a recreational estate.', short_description_es: 'Terreno agrícola de 5.000 m² con ruinas y agua', short_description_ru: 'Сельхоз участок 5000 м² с руинами', + short_description_en: 'Agricultural land 5,000 m² with ruins', address: 'Camino Rural de Güímar', city: 'Güímar', postal_code: '38500', zone: 'Valle de Güímar', lat: 28.3183, lng: -16.4167, area: 5000, price: 125000, price_per_m2: 25, water: 'available', electricity: 'nearby', phone: 'unavailable', drainage: 'unavailable', road: 'dirt', gas: 'unavailable', orientation: 'east', views_sea: 1, views_mountain: 1, views_valley: 1, topography: 'slope', has_ruins: 1, images: '["https://images.unsplash.com/photo-1500382017468-9049fed747ef?w=1920&q=80"]', - videos: '[]', badges: '["new"]', is_featured: 0, published_at: now + badges: '["new"]', is_featured: 0, published_at: now }, { id: 'prop-003', slug: 'villa-los-cristianos', reference: 'TP-003', type: 'house', status: 'active', land_type: 'urban', - title_es: 'Villa de Lujo en Los Cristianos', title_ru: 'Роскошная вилла в Лос-Кристианос', - description_es: 'Espectacular villa de lujo con piscina privada y vistas panorámicas al océano.', - description_ru: 'Потрясающая вилла класса люкс с частным бассейном и панорамным видом на океан.', + title_es: 'Villa de Lujo en Los Cristianos', title_ru: 'Роскошная вилла в Лос-Кристианос', title_en: 'Luxury Villa in Los Cristianos', + description_es: 'Espectacular villa de lujo con piscina privada y vistas panorámicas al océano. 4 dormitorios, 3 baños, jardín tropical completamente equipada. A 5 minutos de la playa.', + description_ru: 'Потрясающая вилла класса люкс с частным бассейном и панорамным видом на океан. 4 спальни, 3 ванные, тропический сад, полностью укомплектована. В 5 минутах от пляжа.', + description_en: 'Spectacular luxury villa with private pool and panoramic ocean views. 4 bedrooms, 3 bathrooms, tropical garden, fully equipped. 5 minutes from the beach.', short_description_es: 'Villa de lujo con piscina, 4 dormitorios', short_description_ru: 'Вилла с бассейном, 4 спальни', + short_description_en: 'Luxury villa with pool, 4 bedrooms', address: 'Urbanización Los Cristianos', city: 'Los Cristianos', postal_code: '38650', zone: 'Los Cristianos', lat: 28.0565, lng: -16.7134, area: 350, price: 595000, price_per_m2: 1700, bedrooms: 4, bathrooms: 3, water: 'available', electricity: 'available', phone: 'available', drainage: 'available', road: 'asphalt', gas: 'available', orientation: 'south', views_sea: 1, has_license: 1, - images: '["https://images.unsplash.com/photo-1613490493576-7fde63acd811?w=1920&q=80"]', - videos: '[]', badges: '["featured"]', is_featured: 1, published_at: now + images: '["https://images.unsplash.com/photo-1613490493576-7fde63acd811?w=1920&q=80","https://images.unsplash.com/photo-1600596542815-ffad4c1539a5?w=800&q=80"]', + badges: '["featured"]', is_featured: 1, published_at: now + }, + { + id: 'prop-004', slug: 'terreno-vista-orotava', reference: 'TP-004', type: 'agricultural', status: 'active', land_type: 'agricultural', + title_es: 'Finca con Vistas al Teide', title_ru: 'Усадьба с видом на Тейде', title_en: 'Estate with Views of Teide', + description_es: 'Espectacular finca de 10.000 m² en La Orotava con vistas directas al Teide. Terreno con viñedo propio, agua de galería y acceso asfaltado. Perfecto para proyecto agrícola o casa rural.', + description_ru: 'Потрясающая усадьба 10000 м² в Ла Оротава с прямым видом на Тейде. Участок с собственным виноградником, галерейной водой и асфальтированным подъездом. Идеально для сельского проекта или сельского дома.', + description_en: 'Spectacular estate of 10,000 m² in La Orotava with direct views of Teide. Land with own vineyard, gallery water and asphalt access. Perfect for agricultural project or rural house.', + short_description_es: 'Finca 10.000 m² con viñedo propio', + short_description_ru: 'Усадьба 10000 м² с виноградником', + short_description_en: 'Estate 10,000 m² with vineyard', + address: 'Camino Viejo de La Orotava', city: 'La Orotava', postal_code: '38300', zone: 'Valle de La Orotava', + lat: 28.3922, lng: -16.5215, area: 10000, price: 285000, price_per_m2: 28, + water: 'available', electricity: 'available', phone: 'nearby', drainage: 'available', road: 'asphalt', gas: 'unavailable', + orientation: 'north', views_sea: 0, views_mountain: 1, views_valley: 1, topography: 'slope', has_ruins: 0, is_buildable: 1, max_floors: 2, + images: '["https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?w=1920&q=80"]', + badges: '["exclusive"]', is_featured: 1, is_exclusive: 1, published_at: now + }, + { + id: 'prop-005', slug: 'apartamento-playa-las-americas', reference: 'TP-005', type: 'apartment', status: 'active', land_type: 'urban', + title_es: 'Apartamento en Playa de las Américas', title_ru: 'Апартаменты на Плая-де-лас-Америкас', title_en: 'Apartment in Playa de las Américas', + description_es: 'Moderno apartamento de 2 dormitorios a solo 100 metros de la playa. Terraza con vistas al mar, piscina comunitaria y parking. Ideal para inversión vacacional.', + description_ru: 'Современные апартаменты с 2 спальнями всего в 100 метрах от пляжа. Терраса с видом на море, общий бассейн и парковка. Идеально для праздничной аренды.', + description_en: 'Modern 2-bedroom apartment just 100 meters from the beach. Terrace with sea views, communal pool and parking. Ideal for holiday rental investment.', + short_description_es: 'Apartamento 2 dorm., 100m de la playa', + short_description_ru: 'Апартаменты 2 сп., 100м от пляжа', + short_description_en: '2-bed apartment, 100m from beach', + address: 'Avda. de Colón', city: 'Arona', postal_code: '38660', zone: 'Playa de las Américas', + lat: 28.0716, lng: -16.7278, area: 85, price: 195000, price_per_m2: 2294, bedrooms: 2, bathrooms: 1, + water: 'available', electricity: 'available', phone: 'available', drainage: 'available', road: 'asphalt', gas: 'available', + orientation: 'south', views_sea: 1, has_license: 1, + images: '["https://images.unsplash.com/photo-1522708323590-d24dbb8d0d4d?w=1920&q=80"]', + badges: '["new","featured"]', is_featured: 1, published_at: now + }, + { + id: 'prop-006', slug: 'terreno-rustico-icod', reference: 'TP-006', type: 'agricultural', status: 'active', land_type: 'agricultural', + title_es: 'Terreno Rústico en Icod de los Vinos', title_ru: 'Сельский участок в Икод-де-лос-Винос', title_en: 'Rustic Land in Icod de los Vinos', + description_es: 'Terreno rústico de 7.500 m² cerca del famoso árbol Drago Milenario. Terreno plano ideal para cultivo de plátanos o como finca de recreo con vistas al mar.', + description_ru: 'Сельский участок 7500 м² рядом со знаменитым деревом Драго Милenario. Плоский участок идеален для выращивания бананов или как усадьба для отдыха с видом на море.', + description_en: 'Rustic land of 7,500 m² near the famous Drago Milenario tree. Flat terrain ideal for banana cultivation or as recreational estate with sea views.', + short_description_es: 'Terreno plano 7.500 m² cerca del Drago', + short_description_ru: 'Плоский участок 7500 м² у Драго', + short_description_en: 'Flat terrain 7,500 m² near Drago', + address: 'Camino del Drago', city: 'Icod de los Vinos', postal_code: '38430', zone: 'Icod', + lat: 28.4736, lng: -16.7178, area: 7500, price: 165000, price_per_m2: 22, + water: 'available', electricity: 'nearby', phone: 'unavailable', drainage: 'unavailable', road: 'dirt', gas: 'unavailable', + orientation: 'west', views_sea: 1, views_mountain: 1, topography: 'flat', has_ruins: 0, + images: '["https://images.unsplash.com/photo-1501854140801-5070f5b2a5c1?w=1920&q=80"]', + badges: '[]', is_featured: 0, published_at: now + }, + { + id: 'prop-007', slug: 'chalet-santa-cruz', reference: 'TP-007', type: 'house', status: 'active', land_type: 'urban', + title_es: 'Chalet en Santa Cruz de Tenerife', title_ru: 'Шале в Санта-Крус-де-Тенерифе', title_en: 'Chalet in Santa Cruz de Tenerife', + description_es: 'Elegante chalet en zona residencial de Santa Cruz. 3 dormitorios, 2 baños, jardín privado y garaje. Zona tranquila con todos los servicios cercanos.', + description_ru: 'Элегантное шале в жилом районе Санта-Крус. 3 спальни, 2 ванные, частный сад и гараж. Тихий район со всеми услугами поблизости.', + description_en: 'Elegant chalet in residential area of Santa Cruz. 3 bedrooms, 2 bathrooms, private garden and garage. Quiet area with all services nearby.', + short_description_es: 'Chalet 3 dorm., jardín privado', + short_description_ru: 'Шале 3 сп., частный сад', + short_description_en: 'Chalet 3 bed, private garden', + address: 'Calle la Arena', city: 'Santa Cruz de Tenerife', postal_code: '38005', zone: 'Somosierra', + lat: 28.4612, lng: -16.2602, area: 220, price: 385000, price_per_m2: 1750, bedrooms: 3, bathrooms: 2, + water: 'available', electricity: 'available', phone: 'available', drainage: 'available', road: 'asphalt', gas: 'available', + orientation: 'southeast', views_sea: 1, views_mountain: 1, has_license: 1, + images: '["https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=1920&q=80"]', + badges: '[]', is_featured: 0, published_at: now + }, + { + id: 'prop-008', slug: 'terreno-urbanizable-granadilla', reference: 'TP-008', type: 'urban', status: 'active', land_type: 'urban', + title_es: 'Terreno Urbanizable en Granadilla', title_ru: 'Участок под застройку в Гранадилье', title_en: 'Developable Land in Granadilla', + description_es: 'Gran terreno urbanizable de 15.000 m² cerca del Aeropuerto del Sur. Perfecto para desarrollo residencial o industrial. A 10 minutos de la playa.', + description_ru: 'Большой участок под застройку 15000 м² рядом с Южным аэропортом. Идеально для жилого или коммерческого развития. В 10 минутах от пляжа.', + description_en: 'Large developable land of 15,000 m² near the South Airport. Perfect for residential or industrial development. 10 minutes from the beach.', + short_description_es: 'Terreno urbanizable 15.000 m²', + short_description_ru: 'Участок под застройку 15000 м²', + short_description_en: 'Developable land 15,000 m²', + address: 'Polígono Industrial Granadilla', city: 'Granadilla de Abona', postal_code: '38600', zone: 'Granadilla', + lat: 28.1189, lng: -16.5678, area: 15000, price: 450000, price_per_m2: 30, + water: 'nearby', electricity: 'nearby', phone: 'nearby', drainage: 'nearby', road: 'paved', gas: 'planned', + orientation: 'south', views_sea: 1, is_buildable: 1, max_floors: 3, + images: '["https://images.unsplash.com/photo-1500676804-8757-0c6b8f3f5c5e?w=1920&q=80"]', + badges: '["new"]', is_featured: 0, published_at: now + }, + { + id: 'prop-009', slug: 'finca-platanos-canarios', reference: 'TP-009', type: 'agricultural', status: 'active', land_type: 'agricultural', + title_es: 'Finca de Plátanos en Los Realejos', title_ru: 'Банановая плантация в Лос-Реалехос', title_en: 'Banana Farm in Los Realejos', + description_es: 'Productiva finca de plátanos de 25.000 m² con sistema de riego instalado y agua propia. Incluye casa rural reformada. Negocio en funcionamiento.', + description_ru: 'Продуктивная банановая плантация 25000 м² с установленной системой орошения и собственной водой. Включает отреставрированный сельский дом. Работающий бизнес.', + description_en: 'Productive banana farm of 25,000 m² with installed irrigation system and own water. Includes renovated rural house. Operating business.', + short_description_es: 'Finca platanera 25.000 m² con casa', + short_description_ru: 'Банановая ферма 25000 м² с домом', + short_description_en: 'Banana farm 25,000 m² with house', + address: 'Camino de la Costa', city: 'Los Realejos', postal_code: '38410', zone: 'Los Realejos', + lat: 28.3783, lng: -16.5833, area: 25000, price: 520000, price_per_m2: 21, + water: 'available', electricity: 'available', phone: 'available', drainage: 'available', road: 'asphalt', gas: 'unavailable', + orientation: 'north', views_sea: 1, views_mountain: 1, topography: 'slope', has_ruins: 0, + images: '["https://images.unsplash.com/photo-1576201834545-1d09720e91f2?w=1920&q=80"]', + badges: '["exclusive"]', is_exclusive: 1, published_at: now + }, + { + id: 'prop-010', slug: 'apartamento-puerto-cruz', reference: 'TP-010', type: 'apartment', status: 'active', land_type: 'urban', + title_es: 'Apartamento en Puerto de la Cruz', title_ru: 'Апартаменты в Пуэрто-де-ла-Крус', title_en: 'Apartment in Puerto de la Cruz', + description_es: 'Acogedor apartamento de 1 dormitorio en el centro de Puerto de la Cruz, a 2 minutos del Lago Martiánez. Reformado con materiales de primera calidad.', + description_ru: 'Уютные апартаменты с 1 спальней в центре Пуэрто-де-ла-Крус, в 2 минутах от Лаго Мартианес. Отремонтирован с первоклассными материалами.', + description_en: 'Cozy 1-bedroom apartment in the center of Puerto de la Cruz, 2 minutes from Lago Martiánez. Renovated with first-class materials.', + short_description_es: 'Apartamento 1 dorm. reformado', + short_description_ru: 'Апартаменты 1 сп. отремонтированы', + short_description_en: 'Renovated 1-bed apartment', + address: 'Calle Iriarte', city: 'Puerto de la Cruz', postal_code: '38400', zone: 'Centro', + lat: 28.4156, lng: -16.5522, area: 55, price: 125000, price_per_m2: 2273, bedrooms: 1, bathrooms: 1, + water: 'available', electricity: 'available', phone: 'available', drainage: 'available', road: 'asphalt', gas: 'available', + orientation: 'south', views_sea: 0, has_license: 1, + images: '["https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?w=1920&q=80"]', + badges: '["new"]', is_featured: 0, published_at: now + }, + // More properties... + { + id: 'prop-011', slug: 'villa-piscina-san-miguel', reference: 'TP-011', type: 'house', status: 'active', land_type: 'urban', + title_es: 'Villa con Piscina en San Miguel', title_ru: 'Вилла с бассейном в Сан-Мигеле', title_en: 'Villa with Pool in San Miguel', + description_es: 'Impresionante villa con piscina privada y jardín de 500 m². 5 dormitorios, 4 baños, vistas al mar y al Teide. Zona muy tranquila.', + description_ru: 'Потрясающая вилла с частным бассейном и садом 500 м². 5 спален, 4 ванные, вид на море и Тейде. Очень тихий район.', + description_en: 'Impressive villa with private pool and 500 m² garden. 5 bedrooms, 4 bathrooms, sea and Teide views. Very quiet area.', + short_description_es: 'Villa 5 dorm., piscina privada', + short_description_ru: 'Вилла 5 сп., част. бассейн', + short_description_en: 'Villa 5 bed, private pool', + address: 'Urbanización San Miguel', city: 'San Miguel de Abona', postal_code: '38620', zone: 'San Miguel', + lat: 28.0945, lng: -16.6189, area: 450, price: 750000, price_per_m2: 1667, bedrooms: 5, bathrooms: 4, + water: 'available', electricity: 'available', phone: 'available', drainage: 'available', road: 'asphalt', gas: 'available', + orientation: 'southwest', views_sea: 1, views_mountain: 1, has_license: 1, + images: '["https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=1920&q=80"]', + badges: '["featured","exclusive"]', is_featured: 1, is_exclusive: 1, published_at: now + }, + { + id: 'prop-012', slug: 'terreno-vistas-candelaria', reference: 'TP-012', type: 'agricultural', status: 'reserved', land_type: 'agricultural', + title_es: 'Terreno con Vistas en Candelaria', title_ru: 'Участок с видом в Канделарии', title_en: 'Land with Views in Candelaria', + description_es: 'Terreno de 3.500 m² en Candelaria con vistas espectaculares al mar. Ideal para construir casa unifamiliar. Agua y luz cerca.', + description_ru: 'Участок 3500 м² в Канделарии со потрясающим видом на море. Идеально для строительства частного дома. Вода и электричество рядом.', + description_en: 'Land of 3,500 m² in Candelaria with spectacular sea views. Ideal for building a single-family home. Water and electricity nearby.', + short_description_es: 'Terreno 3.500 m² vistas al mar', + short_description_ru: 'Участок 3500 м² вид на море', + short_description_en: 'Land 3,500 m² sea views', + address: 'Lomo de las Bayas', city: 'Candelaria', postal_code: '38530', zone: 'Candelaria', + lat: 28.3552, lng: -16.3697, area: 3500, price: 145000, price_per_m2: 41, + water: 'nearby', electricity: 'nearby', phone: 'unavailable', drainage: 'unavailable', road: 'dirt', gas: 'unavailable', + orientation: 'east', views_sea: 1, views_mountain: 1, topography: 'slope', has_ruins: 0, + images: '["https://images.unsplash.com/photo-1502023986840-af7ebe78b7d4?w=1920&q=80"]', + badges: '["new"]', published_at: now } ] const stmt = db.prepare(` INSERT INTO properties ( - id, slug, reference, type, status, land_type, title_es, title_ru, description_es, description_ru, - short_description_es, short_description_ru, address, city, postal_code, zone, lat, lng, area, price, price_per_m2, + id, slug, reference, type, status, land_type, title_es, title_ru, title_en, description_es, description_ru, description_en, + short_description_es, short_description_ru, short_description_en, address, city, postal_code, zone, lat, lng, area, price, price_per_m2, bedrooms, bathrooms, water, electricity, phone, drainage, road, gas, orientation, views_sea, views_mountain, views_valley, topography, has_ruins, has_license, is_buildable, max_floors, images, videos, badges, is_featured, is_exclusive, published_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `) props.forEach(p => { - 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) }