Files
2026-04-30 10:55:23 +03:00

274 lines
9.5 KiB
JavaScript

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}`);
});