first commit

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
shop
2026-05-16 20:52:15 +03:00
parent 3419d90e61
commit 323e0a2926
67 changed files with 1723 additions and 3077 deletions
+116
View File
@@ -0,0 +1,116 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const { db } = require('../db');
const { getCart, cartCount } = require('../cart');
const { requireAuth } = require('../middleware/auth');
const router = express.Router();
router.use((req, res, next) => {
const cart = getCart(req);
res.locals.cartCount = cartCount(cart);
res.locals.formatPrice = require('../db').formatPrice;
next();
});
router.get('/register', (req, res) => {
if (req.session.userId) return res.redirect('/account');
res.render('register', { title: 'Регистрация', error: null, values: {} });
});
router.post('/register', (req, res) => {
const { name, email, password, password2 } = req.body;
const values = { name, email };
if (!name?.trim() || !email?.trim() || !password) {
return res.status(400).render('register', {
title: 'Регистрация',
error: 'Заполните все поля',
values,
});
}
if (password.length < 6) {
return res.status(400).render('register', {
title: 'Регистрация',
error: 'Пароль не менее 6 символов',
values,
});
}
if (password !== password2) {
return res.status(400).render('register', {
title: 'Регистрация',
error: 'Пароли не совпадают',
values,
});
}
const hash = bcrypt.hashSync(password, 10);
try {
const r = db
.prepare('INSERT INTO users (email, password_hash, name) VALUES (?, ?, ?)')
.run(email.trim().toLowerCase(), hash, name.trim());
req.session.userId = r.lastInsertRowid;
res.redirect('/');
} catch (err) {
if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') {
return res.status(400).render('register', {
title: 'Регистрация',
error: 'Этот email уже зарегистрирован',
values,
});
}
throw err;
}
});
router.get('/login', (req, res) => {
if (req.session.userId) return res.redirect('/account');
res.render('login', {
title: 'Вход',
error: null,
next: req.query.next || '/',
values: {},
});
});
router.post('/login', (req, res) => {
const { email, password } = req.body;
const next = req.body.next || '/';
const values = { email };
const user = db
.prepare('SELECT * FROM users WHERE email = ?')
.get((email || '').trim().toLowerCase());
if (!user || !bcrypt.compareSync(password || '', user.password_hash)) {
return res.status(401).render('login', {
title: 'Вход',
error: 'Неверный email или пароль',
next,
values,
});
}
req.session.userId = user.id;
res.redirect(next.startsWith('/') ? next : '/');
});
router.post('/logout', (req, res) => {
req.session.destroy(() => {
res.redirect('/');
});
});
router.get('/account', requireAuth, (req, res) => {
const user = db
.prepare('SELECT id, email, name, created_at FROM users WHERE id = ?')
.get(req.session.userId);
const orderCount = db
.prepare('SELECT COUNT(*) AS n FROM orders WHERE user_id = ?')
.get(user.id).n;
res.render('account', { title: 'Личный кабинет', user, orderCount });
});
module.exports = router;
+267
View File
@@ -0,0 +1,267 @@
const express = require('express');
const { db, formatPrice } = require('../db');
const { getCart, cartCount, cartItems, cartTotal } = require('../cart');
const { requireAuth } = require('../middleware/auth');
const router = express.Router();
function enrichLocals(req, res) {
const cart = getCart(req);
res.locals.cartCount = cartCount(cart);
res.locals.formatPrice = formatPrice;
}
router.use((req, res, next) => {
enrichLocals(req, res);
next();
});
router.get('/', (req, res) => {
const category = req.query.category || '';
const q = (req.query.q || '').trim();
let sql = `
SELECT p.*, c.name AS category_name, c.slug AS category_slug
FROM products p
LEFT JOIN categories c ON c.id = p.category_id
WHERE p.stock > 0
`;
const params = [];
if (category) {
sql += ' AND c.slug = ?';
params.push(category);
}
if (q) {
sql += ' AND (p.name LIKE ? OR p.description LIKE ?)';
const like = `%${q}%`;
params.push(like, like);
}
sql += ' ORDER BY p.name';
const products = db.prepare(sql).all(...params);
const categories = db.prepare('SELECT * FROM categories ORDER BY name').all();
res.render('home', {
title: 'Каталог',
products,
categories,
activeCategory: category,
searchQuery: q,
});
});
router.get('/product/:slug', (req, res) => {
const product = db
.prepare(
`SELECT p.*, c.name AS category_name, c.slug AS category_slug
FROM products p
LEFT JOIN categories c ON c.id = p.category_id
WHERE p.slug = ?`
)
.get(req.params.slug);
if (!product) {
return res.status(404).render('error', {
title: 'Не найдено',
message: 'Товар не найден',
code: 404,
});
}
res.render('product', { title: product.name, product });
});
router.get('/cart', (req, res) => {
const cart = getCart(req);
const items = cartItems(db, cart);
const total = cartTotal(items);
const errorMsg = req.query.error ? decodeURIComponent(String(req.query.error)) : null;
res.render('cart', {
title: 'Корзина',
items,
total,
error: errorMsg,
});
});
router.post('/cart/add', (req, res) => {
const productId = parseInt(req.body.product_id, 10);
const quantity = Math.max(1, parseInt(req.body.quantity, 10) || 1);
const product = db
.prepare('SELECT id, stock FROM products WHERE id = ?')
.get(productId);
if (!product) {
return res.redirect('/');
}
const cart = getCart(req);
const current = cart[productId] || 0;
const nextQty = Math.min(product.stock, current + quantity);
cart[productId] = nextQty;
const redirect = req.body.redirect || '/cart';
res.redirect(redirect);
});
router.post('/cart/update', (req, res) => {
const cart = getCart(req);
const updates = req.body.items || {};
for (const [id, qty] of Object.entries(updates)) {
const productId = parseInt(id, 10);
const quantity = parseInt(qty, 10);
if (!productId) continue;
if (!quantity || quantity <= 0) {
delete cart[productId];
continue;
}
const product = db
.prepare('SELECT stock FROM products WHERE id = ?')
.get(productId);
if (product) {
cart[productId] = Math.min(product.stock, quantity);
}
}
res.redirect('/cart');
});
router.post('/cart/remove/:id', (req, res) => {
const cart = getCart(req);
delete cart[parseInt(req.params.id, 10)];
res.redirect('/cart');
});
router.get('/checkout', requireAuth, (req, res) => {
const cart = getCart(req);
const items = cartItems(db, cart);
if (items.length === 0) {
return res.redirect('/cart');
}
res.render('checkout', {
title: 'Оформление заказа',
items,
total: cartTotal(items),
error: null,
});
});
router.post('/checkout', requireAuth, (req, res) => {
const cart = getCart(req);
const items = cartItems(db, cart);
if (items.length === 0) {
return res.redirect('/cart');
}
const { name, email, phone, address } = req.body;
if (!name?.trim() || !email?.trim() || !address?.trim()) {
return res.status(400).render('checkout', {
title: 'Оформление заказа',
items,
total: cartTotal(items),
error: 'Заполните имя, email и адрес доставки',
});
}
const total = cartTotal(items);
const placeOrder = db.transaction(() => {
for (const item of items) {
const row = db
.prepare('SELECT stock FROM products WHERE id = ?')
.get(item.id);
if (!row || row.stock < item.quantity) {
throw new Error(`Недостаточно «${item.name}» на складе`);
}
}
const order = db
.prepare(
`INSERT INTO orders (user_id, status, total_cents, customer_name, customer_email, customer_phone, address)
VALUES (?, 'pending', ?, ?, ?, ?, ?)`
)
.run(
req.session.userId,
total,
name.trim(),
email.trim(),
(phone || '').trim(),
address.trim()
);
const insertItem = db.prepare(
`INSERT INTO order_items (order_id, product_id, quantity, price_cents)
VALUES (?, ?, ?, ?)`
);
const updateStock = db.prepare(
'UPDATE products SET stock = stock - ? WHERE id = ?'
);
for (const item of items) {
insertItem.run(order.lastInsertRowid, item.id, item.quantity, item.price_cents);
updateStock.run(item.quantity, item.id);
}
return order.lastInsertRowid;
});
try {
const orderId = placeOrder();
req.session.cart = {};
res.redirect(`/orders/${orderId}?success=1`);
} catch (err) {
res.redirect(`/cart?error=${encodeURIComponent(err.message)}`);
}
});
router.get('/orders', requireAuth, (req, res) => {
const orders = db
.prepare(
`SELECT id, status, total_cents, created_at
FROM orders WHERE user_id = ?
ORDER BY created_at DESC`
)
.all(req.session.userId);
res.render('orders', { title: 'Мои заказы', orders });
});
router.get('/orders/:id', requireAuth, (req, res) => {
const order = db
.prepare(
`SELECT * FROM orders WHERE id = ? AND user_id = ?`
)
.get(req.params.id, req.session.userId);
if (!order) {
return res.status(404).render('error', {
title: 'Не найдено',
message: 'Заказ не найден',
code: 404,
});
}
const items = db
.prepare(
`SELECT oi.*, p.name, p.slug, p.image_url
FROM order_items oi
JOIN products p ON p.id = oi.product_id
WHERE oi.order_id = ?`
)
.all(order.id);
res.render('order', {
title: `Заказ #${order.id}`,
order,
items,
success: req.query.success === '1',
});
});
module.exports = router;