const express = require('express'); const { Pool } = require('pg'); const session = require('express-session'); const bcrypt = require('bcryptjs'); const path = require('path'); const https = require('https'); const multer = require('multer'); const app = express(); const PORT = 3006; // Multer config const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, path.join(__dirname, 'public/uploads')), filename: (req, file, cb) => { const ext = path.extname(file.originalname); cb(null, 'project-' + Date.now() + ext); } }); const upload = multer({ storage, limits: { fileSize: 10 * 1024 * 1024 }, fileFilter: (req, file, cb) => { const ok = /\.(jpg|jpeg|png|webp|gif)$/i.test(file.originalname); cb(null, ok); }}); // DB const pool = new Pool({ user: 'postgres', host: 'localhost', database: 'voda_landing', password: 'postgres', port: 5432, }); // Middleware app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(session({ secret: 'voda-landing-secret-' + Date.now(), resave: false, saveUninitialized: false, cookie: { maxAge: 24 * 60 * 60 * 1000 } })); // Static app.use('/admin', express.static(path.join(__dirname, 'admin'))); app.use(express.static('public')); // Admin password (default: admin) const ADMIN_HASH = bcrypt.hashSync('admin', 10); // Auth middleware function requireAuth(req, res, next) { if (req.session && req.session.admin) return next(); res.status(401).json({ error: 'Unauthorized' }); } // === AUTH === app.post('/api/login', async (req, res) => { const { password } = req.body; // Check DB for admin password, fallback to default const r = await pool.query("SELECT value FROM settings WHERE key='admin_password'"); const hash = r.rows.length ? r.rows[0].value : ADMIN_HASH; if (bcrypt.compareSync(password || '', hash)) { req.session.admin = true; res.json({ ok: true }); } else { res.status(401).json({ error: 'Wrong password' }); } }); app.post('/api/logout', (req, res) => { req.session.destroy(); res.json({ ok: true }); }); app.get('/api/auth-check', (req, res) => { res.json({ auth: !!(req.session && req.session.admin) }); }); // === SETTINGS === app.get('/api/settings', requireAuth, async (req, res) => { const r = await pool.query('SELECT key, value FROM settings'); const obj = {}; r.rows.forEach(row => obj[row.key] = row.value); res.json(obj); }); app.put('/api/settings', requireAuth, async (req, res) => { const entries = Object.entries(req.body); for (const [key, value] of entries) { await pool.query( `INSERT INTO settings (key, value, updated_at) VALUES ($1, $2, NOW()) ON CONFLICT (key) DO UPDATE SET value=$2, updated_at=NOW()`, [key, value] ); } res.json({ ok: true }); }); // === SERVICES === app.get('/api/services', requireAuth, async (req, res) => { const r = await pool.query('SELECT * FROM services ORDER BY sort_order'); res.json(r.rows); }); app.post('/api/services', requireAuth, async (req, res) => { const { title, description, icon_svg, sort_order } = req.body; const r = await pool.query( 'INSERT INTO services (title, description, icon_svg, sort_order) VALUES ($1,$2,$3,$4) RETURNING *', [title, description, icon_svg || '', sort_order || 0] ); res.json(r.rows[0]); }); app.put('/api/services/:id', requireAuth, async (req, res) => { const { title, description, icon_svg, sort_order, active } = req.body; const r = await pool.query( 'UPDATE services SET title=$1, description=$2, icon_svg=$3, sort_order=$4, active=$5 WHERE id=$6 RETURNING *', [title, description, icon_svg, sort_order, active, req.params.id] ); res.json(r.rows[0]); }); app.delete('/api/services/:id', requireAuth, async (req, res) => { await pool.query('DELETE FROM services WHERE id=$1', [req.params.id]); res.json({ ok: true }); }); // === BENEFITS === app.get('/api/benefits', requireAuth, async (req, res) => { const r = await pool.query('SELECT * FROM benefits ORDER BY sort_order'); res.json(r.rows); }); app.post('/api/benefits', requireAuth, async (req, res) => { const { title, description, icon_svg, sort_order } = req.body; const r = await pool.query( 'INSERT INTO benefits (title, description, icon_svg, sort_order) VALUES ($1,$2,$3,$4) RETURNING *', [title, description, icon_svg || '', sort_order || 0] ); res.json(r.rows[0]); }); app.put('/api/benefits/:id', requireAuth, async (req, res) => { const { title, description, icon_svg, sort_order, active } = req.body; const r = await pool.query( 'UPDATE benefits SET title=$1, description=$2, icon_svg=$3, sort_order=$4, active=$5 WHERE id=$6 RETURNING *', [title, description, icon_svg, sort_order, active, req.params.id] ); res.json(r.rows[0]); }); app.delete('/api/benefits/:id', requireAuth, async (req, res) => { await pool.query('DELETE FROM benefits WHERE id=$1', [req.params.id]); res.json({ ok: true }); }); // === UPLOAD === app.post('/api/upload', requireAuth, upload.single('image'), (req, res) => { if (!req.file) return res.status(400).json({ error: 'No file' }); res.json({ url: '/uploads/' + req.file.filename }); }); // === PROJECTS === app.get('/api/projects', requireAuth, async (req, res) => { const r = await pool.query('SELECT * FROM projects ORDER BY sort_order'); res.json(r.rows); }); app.post('/api/projects', requireAuth, async (req, res) => { const { title, address, tag, apartments, duration, status, icon_svg, sort_order, image_url } = req.body; const r = await pool.query( 'INSERT INTO projects (title, address, tag, apartments, duration, status, icon_svg, sort_order, image_url) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING *', [title, address, tag, apartments, duration, status || 'Сдано', icon_svg || '', sort_order || 0, image_url || ''] ); res.json(r.rows[0]); }); app.put('/api/projects/:id', requireAuth, async (req, res) => { const { title, address, tag, apartments, duration, status, icon_svg, sort_order, active, image_url } = req.body; const r = await pool.query( 'UPDATE projects SET title=$1, address=$2, tag=$3, apartments=$4, duration=$5, status=$6, icon_svg=$7, sort_order=$8, active=$9, image_url=$10 WHERE id=$11 RETURNING *', [title, address, tag, apartments, duration, status, icon_svg, sort_order, active, image_url || '', req.params.id] ); res.json(r.rows[0]); }); app.delete('/api/projects/:id', requireAuth, async (req, res) => { await pool.query('DELETE FROM projects WHERE id=$1', [req.params.id]); res.json({ ok: true }); }); // === LEADS === app.get('/api/leads', requireAuth, async (req, res) => { const r = await pool.query('SELECT * FROM leads ORDER BY created_at DESC'); res.json(r.rows); }); app.put('/api/leads/:id', requireAuth, async (req, res) => { const { status } = req.body; const r = await pool.query( 'UPDATE leads SET status=$1, updated_at=NOW() WHERE id=$2 RETURNING *', [status, req.params.id] ); res.json(r.rows[0]); }); app.delete('/api/leads/:id', requireAuth, async (req, res) => { await pool.query('DELETE FROM leads WHERE id=$1', [req.params.id]); res.json({ ok: true }); }); // === PUBLIC: Submit lead === app.post('/api/submit-lead', async (req, res) => { const { company, name, phone, message } = req.body; if (!phone) return res.status(400).json({ error: 'Phone required' }); const r = await pool.query( 'INSERT INTO leads (company, name, phone, message) VALUES ($1,$2,$3,$4) RETURNING *', [company || '', name || '', phone, message || ''] ); // Telegram notification try { const tgR = await pool.query("SELECT key, value FROM settings WHERE key IN ('tg_bot_token','tg_chat_id','tg_enabled')"); const tg = {}; tgR.rows.forEach(row => tg[row.key] = row.value); if (tg.tg_enabled === 'true' && tg.tg_bot_token && tg.tg_chat_id) { const text = `🔔 Новая заявка!\n\n🏢 ${company || '—'}\n👤 ${name || '—'}\n📞 ${phone}\n💬 ${message || '—'}`; const url = `https://api.telegram.org/bot${tg.tg_bot_token}/sendMessage`; const data = JSON.stringify({ chat_id: tg.tg_chat_id, text, parse_mode: 'HTML' }); const req2 = https.request(url, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); req2.write(data); req2.end(); } } catch (e) { console.error('TG error:', e.message); } res.json({ ok: true, id: r.rows[0].id }); }); // === PUBLIC: Get landing data === app.get('/api/landing-data', async (req, res) => { const [settingsR, servicesR, benefitsR, projectsR] = await Promise.all([ pool.query('SELECT key, value FROM settings'), pool.query('SELECT * FROM services WHERE active=true ORDER BY sort_order'), pool.query('SELECT * FROM benefits WHERE active=true ORDER BY sort_order'), pool.query('SELECT * FROM projects WHERE active=true ORDER BY sort_order'), ]); const settings = {}; settingsR.rows.forEach(row => settings[row.key] = row.value); res.json({ settings, services: servicesR.rows, benefits: benefitsR.rows, projects: projectsR.rows }); }); // === CHANGE PASSWORD === app.put('/api/change-password', requireAuth, async (req, res) => { const { password } = req.body; if (!password || password.length < 4) return res.status(400).json({ error: 'Min 4 chars' }); const hash = bcrypt.hashSync(password, 10); await pool.query( `INSERT INTO settings (key, value, updated_at) VALUES ('admin_password', $1, NOW()) ON CONFLICT (key) DO UPDATE SET value=$1, updated_at=NOW()`, [hash] ); res.json({ ok: true }); }); app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });