Initial commit: Voda Landing (Cherepovets)
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user