feat: PostgreSQL 17 вместо SQLite
pg + connect-pg-simple, async routes, docker-compose, скрипт setup-postgres. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,3 +3,11 @@ HOST=127.0.0.1
|
|||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
TRUST_PROXY=1
|
TRUST_PROXY=1
|
||||||
SESSION_SECRET=change-me-to-a-long-random-string
|
SESSION_SECRET=change-me-to-a-long-random-string
|
||||||
|
|
||||||
|
# PostgreSQL 17 (одна строка или отдельные переменные)
|
||||||
|
DATABASE_URL=postgresql://shop:shop@127.0.0.1:5432/shop
|
||||||
|
# PGHOST=127.0.0.1
|
||||||
|
# PGPORT=5432
|
||||||
|
# PGUSER=shop
|
||||||
|
# PGPASSWORD=shop
|
||||||
|
# PGDATABASE=shop
|
||||||
|
|||||||
+1
-4
@@ -1,6 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
data/
|
|
||||||
.env
|
.env
|
||||||
*.db
|
data/
|
||||||
*.db-journal
|
|
||||||
sessions.db
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Shop
|
# Shop
|
||||||
|
|
||||||
Интернет-магазин на **Node.js** с локальной базой **SQLite**.
|
Интернет-магазин на **Node.js** и **PostgreSQL 17**.
|
||||||
|
|
||||||
## Возможности
|
## Возможности
|
||||||
|
|
||||||
@@ -12,119 +12,125 @@
|
|||||||
## Требования
|
## Требования
|
||||||
|
|
||||||
- Node.js 18+
|
- Node.js 18+
|
||||||
|
- PostgreSQL 17
|
||||||
- npm
|
- npm
|
||||||
- На Linux для сборки `better-sqlite3`: `build-essential`, `python3`
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PostgreSQL 17
|
||||||
|
|
||||||
|
### Docker (разработка / тест)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
# БД: postgresql://shop:shop@127.0.0.1:5432/shop
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ubuntu (сервер)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apt update
|
||||||
|
apt install -y postgresql-17 postgresql-client-17
|
||||||
|
|
||||||
|
# Пользователь и база shop
|
||||||
|
cd /opt/shop
|
||||||
|
sudo bash scripts/setup-postgres-ubuntu.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Схема таблиц: `postgres/init/01_schema.sql` (применяется при старте приложения и при первом запуске Docker).
|
||||||
|
|
||||||
|
Сессии хранятся в PostgreSQL (таблица `session`, создаётся автоматически).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Быстрый развёртывание на Ubuntu
|
## Быстрый развёртывание на Ubuntu
|
||||||
|
|
||||||
Скопируйте блок целиком на чистый сервер Ubuntu 22.04 / 24.04 (от root или через `sudo`):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Зависимости системы
|
# 1. Система + Node.js 20
|
||||||
apt update
|
apt update
|
||||||
apt install -y git curl build-essential python3
|
apt install -y git curl
|
||||||
|
|
||||||
# 2. Node.js 20 LTS
|
|
||||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
||||||
apt install -y nodejs
|
apt install -y nodejs
|
||||||
|
|
||||||
# 3. Клонирование (подставьте URL вашего репозитория)
|
# 2. PostgreSQL 17
|
||||||
|
apt install -y postgresql-17 postgresql-client-17
|
||||||
|
|
||||||
|
# 3. Код
|
||||||
cd /opt
|
cd /opt
|
||||||
git clone <URL_РЕПОЗИТОРИЯ> shop
|
git clone <URL_РЕПОЗИТОРИЯ> shop
|
||||||
cd shop
|
cd shop
|
||||||
|
|
||||||
# 4. Настройка окружения
|
# 4. БД
|
||||||
|
bash scripts/setup-postgres-ubuntu.sh
|
||||||
|
|
||||||
|
# 5. Окружение
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# Сгенерируйте секрет сессии:
|
|
||||||
sed -i "s/change-me-to-a-long-random-string/$(openssl rand -hex 32)/" .env
|
sed -i "s/change-me-to-a-long-random-string/$(openssl rand -hex 32)/" .env
|
||||||
|
# Проверьте DATABASE_URL в .env
|
||||||
|
|
||||||
# 5. Установка приложения
|
# 6. Приложение
|
||||||
npm install --omit=dev
|
npm install --omit=dev
|
||||||
|
|
||||||
# 6. Caddy (HTTPS + прокси) — см. раздел ниже; для проверки без домена:
|
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
Без Caddy сайт на **http://IP_СЕРВЕРА:3000**. С Caddy — **https://ваш-домен** (любой, указанный в `Caddyfile`).
|
Проверка:
|
||||||
|
|
||||||
В `.env` для production задайте (уже есть в `.env.example`):
|
```bash
|
||||||
|
curl -s http://127.0.0.1:3000/health
|
||||||
|
# {"ok":true,"service":"shop","database":"postgresql"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Переменные `.env`
|
||||||
|
|
||||||
```env
|
```env
|
||||||
PORT=3000
|
PORT=3000
|
||||||
HOST=127.0.0.1
|
HOST=127.0.0.1
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
TRUST_PROXY=1
|
TRUST_PROXY=1
|
||||||
SESSION_SECRET=ваш-длинный-секрет
|
SESSION_SECRET=длинный-секрет
|
||||||
|
DATABASE_URL=postgresql://shop:shop@127.0.0.1:5432/shop
|
||||||
```
|
```
|
||||||
|
|
||||||
`HOST=127.0.0.1` — Node слушает только localhost; снаружи доступ через Caddy.
|
| Переменная | Описание |
|
||||||
|
|------------|----------|
|
||||||
При первом запуске создаются `data/shop.db`, `data/sessions.db` и демо-товары.
|
| `DATABASE_URL` | Строка подключения PostgreSQL |
|
||||||
|
| `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE` | Альтернатива `DATABASE_URL` |
|
||||||
|
| `HOST` | `127.0.0.1` в production (доступ через Caddy) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Запуск как служба (systemd)
|
## Запуск как служба (systemd)
|
||||||
|
|
||||||
Чтобы магазин работал после перезагрузки сервера:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp /opt/shop/deploy/shop.service /etc/systemd/system/shop.service
|
cp /opt/shop/deploy/shop.service /etc/systemd/system/shop.service
|
||||||
|
|
||||||
# Код обновляйте от root; НЕ делайте chown -R на весь /opt/shop (ломает git pull)
|
|
||||||
cd /opt/shop
|
cd /opt/shop
|
||||||
git config --global --add safe.directory /opt/shop
|
git config --global --add safe.directory /opt/shop
|
||||||
git pull
|
|
||||||
npm install --omit=dev
|
npm install --omit=dev
|
||||||
|
|
||||||
# Запись только в data/ — для пользователя службы www-data
|
# Код — root, служба — www-data (только чтение кода достаточно)
|
||||||
mkdir -p /opt/shop/data
|
|
||||||
chown -R www-data:www-data /opt/shop/data
|
|
||||||
chmod +x /opt/shop/scripts/diagnose-502.sh 2>/dev/null || true
|
|
||||||
|
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable shop
|
systemctl enable shop
|
||||||
systemctl start shop
|
systemctl start shop
|
||||||
systemctl status shop
|
journalctl -u shop -f
|
||||||
|
|
||||||
# Backend должен ответить:
|
|
||||||
curl -s http://127.0.0.1:3000/health
|
|
||||||
# {"ok":true,"service":"shop"}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Логи: `journalctl -u shop -f`
|
`EnvironmentFile=/opt/shop/.env` должен содержать `DATABASE_URL`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Проверка после установки
|
## Проверка после установки
|
||||||
|
|
||||||
Перед настройкой Caddy убедитесь, что Node отвечает локально:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/shop
|
|
||||||
systemctl restart shop
|
systemctl restart shop
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|
||||||
curl -s http://127.0.0.1:3000/health
|
curl -s http://127.0.0.1:3000/health
|
||||||
# {"ok":true,"service":"shop"}
|
|
||||||
|
|
||||||
ss -tlnp | grep 3000
|
ss -tlnp | grep 3000
|
||||||
# LISTEN ... 127.0.0.1:3000 ... node
|
|
||||||
|
|
||||||
journalctl -u shop -n 5 --no-pager
|
journalctl -u shop -n 5 --no-pager
|
||||||
# должна быть строка: Магазин: http://127.0.0.1:3000
|
# Магазин: http://127.0.0.1:3000 (PostgreSQL)
|
||||||
```
|
```
|
||||||
|
|
||||||
Если в логе только «База уже содержит товары…» и служба сразу останавливается — обновите код: `git pull` (нужна актуальная версия `seed.js`).
|
Обновление:
|
||||||
|
|
||||||
После успешной проверки настройте Caddy и выполните:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
systemctl reload caddy
|
|
||||||
```
|
|
||||||
|
|
||||||
Обновление на сервере одной командой:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash /opt/shop/scripts/server-update.sh
|
bash /opt/shop/scripts/server-update.sh
|
||||||
@@ -132,17 +138,9 @@ bash /opt/shop/scripts/server-update.sh
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Caddy — SSL и reverse proxy (рекомендуется)
|
## Caddy — SSL и reverse proxy
|
||||||
|
|
||||||
[Caddy](https://caddyserver.com/) автоматически выпускает и продлевает сертификаты Let's Encrypt.
|
**Перед Caddy:** `curl http://127.0.0.1:3000/health` → OK.
|
||||||
|
|
||||||
**Перед установкой:**
|
|
||||||
|
|
||||||
1. Домен указывает на IP сервера (A-запись).
|
|
||||||
2. Открыты порты **80** и **443** в файрволе.
|
|
||||||
3. Служба `shop` запущена и слушает `127.0.0.1:3000`.
|
|
||||||
|
|
||||||
### Установка Caddy на Ubuntu
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
apt install -y debian-keyring debian-archive-keyring apt-transport-https
|
apt install -y debian-keyring debian-archive-keyring apt-transport-https
|
||||||
@@ -150,21 +148,17 @@ curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
|
|||||||
| gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
| gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
|
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
|
||||||
| tee /etc/apt/sources.list.d/caddy-stable.list
|
| tee /etc/apt/sources.list.d/caddy-stable.list
|
||||||
apt update
|
apt update && apt install -y caddy
|
||||||
apt install -y caddy
|
|
||||||
```
|
|
||||||
|
|
||||||
### Конфигурация
|
|
||||||
|
|
||||||
В репозитории лежит пример: `caddy/Caddyfile.example`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Замените shop.example.com и email на свои
|
|
||||||
cp /opt/shop/caddy/Caddyfile.example /etc/caddy/Caddyfile
|
cp /opt/shop/caddy/Caddyfile.example /etc/caddy/Caddyfile
|
||||||
nano /etc/caddy/Caddyfile
|
nano /etc/caddy/Caddyfile # ваш домен и email
|
||||||
|
|
||||||
|
caddy validate --config /etc/caddy/Caddyfile
|
||||||
|
systemctl enable caddy
|
||||||
|
systemctl reload caddy
|
||||||
```
|
```
|
||||||
|
|
||||||
Пример `/etc/caddy/Caddyfile`:
|
Пример `Caddyfile`:
|
||||||
|
|
||||||
```caddyfile
|
```caddyfile
|
||||||
{
|
{
|
||||||
@@ -177,178 +171,59 @@ shop.example.com {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Проверка и перезапуск:
|
Порты **80/443** открыть; **5432** и **3000** наружу не публиковать.
|
||||||
|
|
||||||
```bash
|
### HTTP 502
|
||||||
caddy validate --config /etc/caddy/Caddyfile
|
|
||||||
systemctl enable caddy
|
|
||||||
systemctl reload caddy
|
|
||||||
systemctl status caddy
|
|
||||||
```
|
|
||||||
|
|
||||||
Сайт откроется по домену из блока `Caddyfile` (в примере — `shop.example.com`).
|
|
||||||
|
|
||||||
Логи Caddy: `journalctl -u caddy -f`
|
|
||||||
|
|
||||||
### Файрвол (ufw)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ufw allow 22/tcp
|
|
||||||
ufw allow 80/tcp
|
|
||||||
ufw allow 443/tcp
|
|
||||||
ufw enable
|
|
||||||
```
|
|
||||||
|
|
||||||
Порт **3000** наружу не открывайте — к приложению ходят только через Caddy.
|
|
||||||
|
|
||||||
### Порядок запуска после перезагрузки
|
|
||||||
|
|
||||||
1. `shop` (Node.js на `127.0.0.1:3000`)
|
|
||||||
2. `caddy` (прокси + HTTPS)
|
|
||||||
|
|
||||||
### HTTP 502 при рабочем SSL
|
|
||||||
|
|
||||||
**SSL есть, 502 — значит Caddy жив, а Node на `127.0.0.1:3000` не отвечает.**
|
|
||||||
|
|
||||||
На сервере выполните:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash /opt/shop/scripts/diagnose-502.sh
|
bash /opt/shop/scripts/diagnose-502.sh
|
||||||
journalctl -u shop -n 50 --no-pager
|
journalctl -u shop -n 50 --no-pager
|
||||||
curl -v http://127.0.0.1:3000/health
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Частые причины и исправление:**
|
| Причина | Решение |
|
||||||
|
|
||||||
| Причина | Что сделать |
|
|
||||||
|--------|-------------|
|
|
||||||
| Служба `shop` не запущена или падает | `systemctl restart shop`, смотрите логи `journalctl -u shop -f` |
|
|
||||||
| Нет `npm install` / сломан `better-sqlite3` | `cd /opt/shop && npm install --omit=dev` (нужны `build-essential`, `python3`) |
|
|
||||||
| Нет прав на `data/` у `www-data` | `mkdir -p /opt/shop/data && chown -R www-data:www-data /opt/shop/data` |
|
|
||||||
| `dubious ownership` / нет `git pull` | `chown -R root:root /opt/shop` + `safe.directory` (см. выше) |
|
|
||||||
| В `.env` нет `HOST`/`PORT` | `HOST=127.0.0.1`, `PORT=3000`, затем `systemctl restart shop` |
|
|
||||||
| Неверный путь к `node` в systemd | `which node` → подставьте в `ExecStart` в `/etc/systemd/system/shop.service` |
|
|
||||||
| Caddy стартовал раньше shop | `cp deploy/caddy-after-shop.conf /etc/systemd/system/caddy.service.d/shop.conf` и `daemon-reload` |
|
|
||||||
| В логе seed и сразу `Deactivated` | `git pull` — старый `seed.js` вызывал `process.exit` до старта сервера |
|
|
||||||
|
|
||||||
**Быстрое восстановление:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/shop
|
|
||||||
git config --global --add safe.directory /opt/shop
|
|
||||||
git pull
|
|
||||||
npm install --omit=dev
|
|
||||||
mkdir -p data
|
|
||||||
chown -R www-data:www-data /opt/shop/data
|
|
||||||
systemctl restart shop
|
|
||||||
curl -s http://127.0.0.1:3000/health # должен быть {"ok":true,...}
|
|
||||||
systemctl reload caddy
|
|
||||||
```
|
|
||||||
|
|
||||||
**Ошибка `dubious ownership` при `git pull`:**
|
|
||||||
|
|
||||||
Вы сделали `chown -R www-data` на весь каталог. Верните владельца репозиторию root и оставьте `data/` за www-data:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chown -R root:root /opt/shop
|
|
||||||
chown -R www-data:www-data /opt/shop/data
|
|
||||||
git config --global --add safe.directory /opt/shop
|
|
||||||
git pull
|
|
||||||
```
|
|
||||||
|
|
||||||
Пока `curl http://127.0.0.1:3000/health` не возвращает OK — HTTPS через Caddy будет отдавать 502.
|
|
||||||
|
|
||||||
### Другие проблемы
|
|
||||||
|
|
||||||
| Симптом | Решение |
|
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
| Нет сертификата | DNS, порты 80/443, верный домен в `Caddyfile` |
|
| PostgreSQL не запущен | `systemctl start postgresql` |
|
||||||
| Редирект-цикл / нет cookies | В `.env`: `TRUST_PROXY=1`, `NODE_ENV=production` |
|
| Неверный `DATABASE_URL` | проверить `.env`, `psql` |
|
||||||
|
| Node не слушает 3000 | `journalctl -u shop -f` |
|
||||||
---
|
| Caddy без backend | сначала `curl /health`, потом `reload caddy` |
|
||||||
|
|
||||||
## Обновление с Git
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/shop
|
|
||||||
bash scripts/server-update.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Или вручную:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/shop
|
|
||||||
git config --global --add safe.directory /opt/shop
|
|
||||||
git pull
|
|
||||||
npm install --omit=dev
|
|
||||||
mkdir -p data && chown -R www-data:www-data data
|
|
||||||
systemctl restart shop
|
|
||||||
curl -s http://127.0.0.1:3000/health
|
|
||||||
systemctl reload caddy # если меняли Caddyfile
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Локальная разработка
|
## Локальная разработка
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
docker compose up -d
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
|
npm install
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Сайт: [http://localhost:3000](http://localhost:3000)
|
|
||||||
|
|
||||||
## Переменные окружения
|
|
||||||
|
|
||||||
| Переменная | Описание | По умолчанию |
|
|
||||||
|------------------|---------------------------|----------------|
|
|
||||||
| `PORT` | Порт HTTP-сервера | `3000` |
|
|
||||||
| `HOST` | Адрес привязки | `0.0.0.0` (dev), `127.0.0.1` (prod) |
|
|
||||||
| `NODE_ENV` | Режим (`production` — secure cookies) | — |
|
|
||||||
| `TRUST_PROXY` | Доверять заголовкам Caddy (`1`) | — |
|
|
||||||
| `SESSION_SECRET` | Секрет для сессий | dev-значение |
|
|
||||||
|
|
||||||
## Скрипты npm
|
## Скрипты npm
|
||||||
|
|
||||||
| Команда | Описание |
|
| Команда | Описание |
|
||||||
|----------------|----------------------------------|
|
|---------|----------|
|
||||||
| `npm start` | Запуск сервера (production) |
|
| `npm start` | Запуск сервера |
|
||||||
| `npm run dev` | Запуск с автоперезагрузкой |
|
| `npm run dev` | С автоперезагрузкой |
|
||||||
| `npm run seed` | Заполнение каталога, если пуст |
|
| `npm run seed` | Демо-товары (если каталог пуст) |
|
||||||
|
|
||||||
## База данных
|
## Структура
|
||||||
|
|
||||||
Локально в каталоге `data/`:
|
|
||||||
|
|
||||||
- `shop.db` — товары, пользователи, заказы
|
|
||||||
- `sessions.db` — сессии
|
|
||||||
|
|
||||||
Каталог `data/` в git не попадает (см. `.gitignore`). Делайте резервные копии:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp -a data/shop.db data/shop.db.bak
|
|
||||||
```
|
|
||||||
|
|
||||||
## Структура проекта
|
|
||||||
|
|
||||||
```
|
```
|
||||||
caddy/
|
postgres/init/01_schema.sql
|
||||||
Caddyfile.example — пример reverse proxy + SSL
|
docker-compose.yml — PostgreSQL 17 локально
|
||||||
deploy/
|
caddy/Caddyfile.example
|
||||||
shop.service — unit для systemd
|
deploy/shop.service
|
||||||
scripts/
|
scripts/
|
||||||
diagnose-502.sh — проверка при 502
|
setup-postgres-ubuntu.sh
|
||||||
server-update.sh — git pull + restart + health
|
diagnose-502.sh
|
||||||
|
server-update.sh
|
||||||
src/
|
src/
|
||||||
server.js — точка входа
|
|
||||||
db.js — схема SQLite
|
|
||||||
seed.js — демо-данные
|
|
||||||
routes/ — маршруты
|
|
||||||
views/ — шаблоны EJS
|
|
||||||
public/css/ — стили
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Миграция с SQLite
|
||||||
|
|
||||||
|
Старая версия хранила данные в `data/*.db`. После перехода на PostgreSQL выполните `git pull`, настройте `DATABASE_URL`, `npm install` — при первом запуске создастся схема и демо-каталог (если таблица `products` пуста). Пользователей и заказы из SQLite нужно переносить вручную или заново зарегистрироваться.
|
||||||
|
|
||||||
## Репозиторий
|
## Репозиторий
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17
|
||||||
|
container_name: shop-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: shop
|
||||||
|
POSTGRES_PASSWORD: shop
|
||||||
|
POSTGRES_DB: shop
|
||||||
|
ports:
|
||||||
|
- '5432:5432'
|
||||||
|
volumes:
|
||||||
|
- shop_pg_data:/var/lib/postgresql/data
|
||||||
|
- ./postgres/init:/docker-entrypoint-initdb.d:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'pg_isready -U shop -d shop']
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
shop_pg_data:
|
||||||
+5
-5
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "shop",
|
"name": "shop",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"description": "Интернет-магазин на Node.js с локальной SQLite",
|
"description": "Интернет-магазин на Node.js с PostgreSQL 17",
|
||||||
"main": "src/server.js",
|
"main": "src/server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node src/server.js",
|
"start": "node src/server.js",
|
||||||
@@ -13,10 +13,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"better-sqlite3": "^11.7.0",
|
"connect-pg-simple": "^10.0.0",
|
||||||
"connect-sqlite3": "^0.9.15",
|
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-session": "^1.18.1"
|
"express-session": "^1.18.1",
|
||||||
|
"pg": "^8.13.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
-- PostgreSQL 17 — схема магазина
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS categories (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
slug TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS products (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
|
||||||
|
slug TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
price_cents INTEGER NOT NULL CHECK (price_cents >= 0),
|
||||||
|
stock INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0),
|
||||||
|
image_url TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS orders (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending'
|
||||||
|
CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled')),
|
||||||
|
total_cents INTEGER NOT NULL CHECK (total_cents >= 0),
|
||||||
|
customer_name TEXT NOT NULL,
|
||||||
|
customer_email TEXT NOT NULL,
|
||||||
|
customer_phone TEXT NOT NULL DEFAULT '',
|
||||||
|
address TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS order_items (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
order_id INTEGER NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
|
||||||
|
product_id INTEGER NOT NULL REFERENCES products(id),
|
||||||
|
quantity INTEGER NOT NULL CHECK (quantity > 0),
|
||||||
|
price_cents INTEGER NOT NULL CHECK (price_cents >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_products_category ON products(category_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_orders_user ON orders(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_products_name ON products(name);
|
||||||
+23
-24
@@ -1,53 +1,52 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Диагностика HTTP 502 (Caddy не достучался до Node)
|
# Диагностика HTTP 502 (Caddy не достучался до Node / БД)
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo "=== Shop / Caddy 502 diagnostic ==="
|
echo "=== Shop / Caddy 502 diagnostic ==="
|
||||||
echo
|
echo
|
||||||
|
|
||||||
echo "1. Служба shop"
|
echo "1. PostgreSQL"
|
||||||
systemctl is-active shop 2>/dev/null || echo " shop: не установлена или не active"
|
if command -v pg_isready >/dev/null; then
|
||||||
systemctl status shop --no-pager -l 2>/dev/null | head -20 || true
|
pg_isready -h 127.0.0.1 -p 5432 && echo " pg_isready: OK" || echo " pg_isready: FAIL"
|
||||||
|
else
|
||||||
|
echo " pg_isready не найден"
|
||||||
|
fi
|
||||||
echo
|
echo
|
||||||
|
|
||||||
echo "2. Служба caddy"
|
echo "2. Служба shop"
|
||||||
|
systemctl is-active shop 2>/dev/null || echo " shop: не active"
|
||||||
|
systemctl status shop --no-pager -l 2>/dev/null | head -15 || true
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "3. Служба caddy"
|
||||||
systemctl is-active caddy 2>/dev/null || true
|
systemctl is-active caddy 2>/dev/null || true
|
||||||
echo
|
echo
|
||||||
|
|
||||||
echo "3. Порт 3000"
|
echo "4. Порт 3000"
|
||||||
if command -v ss >/dev/null; then
|
if command -v ss >/dev/null; then
|
||||||
ss -tlnp | grep ':3000' || echo " Ничего не слушает порт 3000 — запустите shop"
|
ss -tlnp | grep ':3000' || echo " Ничего не слушает порт 3000"
|
||||||
else
|
else
|
||||||
netstat -tlnp 2>/dev/null | grep ':3000' || echo " Порт 3000 не слушается"
|
netstat -tlnp 2>/dev/null | grep ':3000' || true
|
||||||
fi
|
fi
|
||||||
echo
|
echo
|
||||||
|
|
||||||
echo "4. curl backend"
|
echo "5. curl backend"
|
||||||
if curl -sf --max-time 3 http://127.0.0.1:3000/health; then
|
if curl -sf --max-time 3 http://127.0.0.1:3000/health; then
|
||||||
echo
|
echo
|
||||||
echo " OK — Node отвечает, проблема скорее в Caddyfile"
|
echo " OK"
|
||||||
else
|
else
|
||||||
echo " FAIL — Node не отвечает на 127.0.0.1:3000"
|
echo " FAIL — проверьте DATABASE_URL и journalctl -u shop"
|
||||||
echo " Проверьте: journalctl -u shop -n 50 --no-pager"
|
|
||||||
fi
|
fi
|
||||||
echo
|
echo
|
||||||
|
|
||||||
echo "5. .env (HOST, PORT)"
|
echo "6. .env"
|
||||||
if [ -f /opt/shop/.env ]; then
|
if [ -f /opt/shop/.env ]; then
|
||||||
grep -E '^(HOST|PORT|NODE_ENV)=' /opt/shop/.env || true
|
grep -E '^(DATABASE_URL|HOST|PORT)=' /opt/shop/.env 2>/dev/null | sed 's/=.*/=***/' || true
|
||||||
|
grep -E '^DATABASE_URL=' /opt/shop/.env || echo " DATABASE_URL не задан"
|
||||||
else
|
else
|
||||||
echo " /opt/shop/.env не найден"
|
echo " /opt/shop/.env не найден"
|
||||||
fi
|
fi
|
||||||
echo
|
echo
|
||||||
|
|
||||||
echo "6. Права data/"
|
|
||||||
if [ -d /opt/shop/data ]; then
|
|
||||||
ls -la /opt/shop/data
|
|
||||||
else
|
|
||||||
echo " Каталог data/ отсутствует — создайте: mkdir -p /opt/shop/data && chown www-data:www-data /opt/shop/data"
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo "7. Node"
|
echo "7. Node"
|
||||||
which node || which nodejs || echo " node не найден в PATH"
|
which node && node -v
|
||||||
node -v 2>/dev/null || nodejs -v 2>/dev/null || true
|
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ git pull
|
|||||||
|
|
||||||
npm install --omit=dev
|
npm install --omit=dev
|
||||||
|
|
||||||
mkdir -p data
|
|
||||||
chown -R www-data:www-data data
|
|
||||||
|
|
||||||
if systemctl is-active --quiet shop 2>/dev/null; then
|
if systemctl is-active --quiet shop 2>/dev/null; then
|
||||||
systemctl restart shop
|
systemctl restart shop
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# PostgreSQL 17 на Ubuntu — пользователь и БД для магазина
|
||||||
|
# Запуск: sudo bash scripts/setup-postgres-ubuntu.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DB_USER="${DB_USER:-shop}"
|
||||||
|
DB_PASS="${DB_PASS:-shop}"
|
||||||
|
DB_NAME="${DB_NAME:-shop}"
|
||||||
|
|
||||||
|
if ! command -v psql >/dev/null; then
|
||||||
|
echo "Установите PostgreSQL 17:"
|
||||||
|
echo " apt install -y postgresql-17 postgresql-client-17"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo -u postgres psql -v ON_ERROR_STOP=1 <<EOF
|
||||||
|
DO \$\$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${DB_USER}') THEN
|
||||||
|
CREATE ROLE ${DB_USER} LOGIN PASSWORD '${DB_PASS}';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
\$\$;
|
||||||
|
|
||||||
|
SELECT 'CREATE DATABASE ${DB_NAME} OWNER ${DB_USER}'
|
||||||
|
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${DB_NAME}')\gexec
|
||||||
|
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "DATABASE_URL=postgresql://${DB_USER}:${DB_PASS}@127.0.0.1:5432/${DB_NAME}"
|
||||||
+8
-5
@@ -1,3 +1,5 @@
|
|||||||
|
const { query } = require('./db');
|
||||||
|
|
||||||
function getCart(req) {
|
function getCart(req) {
|
||||||
if (!req.session.cart) {
|
if (!req.session.cart) {
|
||||||
req.session.cart = {};
|
req.session.cart = {};
|
||||||
@@ -9,14 +11,15 @@ function cartCount(cart) {
|
|||||||
return Object.values(cart).reduce((sum, qty) => sum + qty, 0);
|
return Object.values(cart).reduce((sum, qty) => sum + qty, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cartItems(db, cart) {
|
async function cartItems(cart) {
|
||||||
const ids = Object.keys(cart).map(Number).filter(Boolean);
|
const ids = Object.keys(cart).map(Number).filter(Boolean);
|
||||||
if (ids.length === 0) return [];
|
if (ids.length === 0) return [];
|
||||||
|
|
||||||
const placeholders = ids.map(() => '?').join(',');
|
const placeholders = ids.map((_, i) => `$${i + 1}`).join(',');
|
||||||
const products = db
|
const { rows: products } = await query(
|
||||||
.prepare(`SELECT * FROM products WHERE id IN (${placeholders})`)
|
`SELECT * FROM products WHERE id IN (${placeholders})`,
|
||||||
.all(...ids);
|
ids
|
||||||
|
);
|
||||||
|
|
||||||
return products
|
return products
|
||||||
.map((p) => ({
|
.map((p) => ({
|
||||||
|
|||||||
@@ -1,69 +1,39 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const Database = require('better-sqlite3');
|
const { Pool } = require('pg');
|
||||||
|
|
||||||
const dataDir = path.join(__dirname, '..', 'data');
|
function buildPoolConfig() {
|
||||||
if (!fs.existsSync(dataDir)) {
|
if (process.env.DATABASE_URL) {
|
||||||
fs.mkdirSync(dataDir, { recursive: true });
|
return { connectionString: process.env.DATABASE_URL };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
host: process.env.PGHOST || '127.0.0.1',
|
||||||
|
port: parseInt(process.env.PGPORT || '5432', 10),
|
||||||
|
user: process.env.PGUSER || 'shop',
|
||||||
|
password: process.env.PGPASSWORD || 'shop',
|
||||||
|
database: process.env.PGDATABASE || 'shop',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const dbPath = path.join(dataDir, 'shop.db');
|
const pool = new Pool(buildPoolConfig());
|
||||||
const db = new Database(dbPath);
|
|
||||||
|
|
||||||
db.pragma('journal_mode = WAL');
|
pool.on('error', (err) => {
|
||||||
db.pragma('foreign_keys = ON');
|
console.error('PostgreSQL pool error:', err.message);
|
||||||
|
});
|
||||||
|
|
||||||
db.exec(`
|
async function query(text, params) {
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
return pool.query(text, params);
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
}
|
||||||
email TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
|
||||||
password_hash TEXT NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS categories (
|
async function initSchema() {
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
const schemaPath = path.join(__dirname, '..', 'postgres', 'init', '01_schema.sql');
|
||||||
slug TEXT NOT NULL UNIQUE,
|
const sql = fs.readFileSync(schemaPath, 'utf8');
|
||||||
name TEXT NOT NULL
|
await pool.query(sql);
|
||||||
);
|
}
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS products (
|
async function checkConnection() {
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
await pool.query('SELECT 1');
|
||||||
category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
|
}
|
||||||
slug TEXT NOT NULL UNIQUE,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
description TEXT NOT NULL DEFAULT '',
|
|
||||||
price_cents INTEGER NOT NULL CHECK (price_cents >= 0),
|
|
||||||
stock INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0),
|
|
||||||
image_url TEXT,
|
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS orders (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
||||||
status TEXT NOT NULL DEFAULT 'pending'
|
|
||||||
CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled')),
|
|
||||||
total_cents INTEGER NOT NULL CHECK (total_cents >= 0),
|
|
||||||
customer_name TEXT NOT NULL,
|
|
||||||
customer_email TEXT NOT NULL,
|
|
||||||
customer_phone TEXT NOT NULL DEFAULT '',
|
|
||||||
address TEXT NOT NULL,
|
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS order_items (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
order_id INTEGER NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
|
|
||||||
product_id INTEGER NOT NULL REFERENCES products(id),
|
|
||||||
quantity INTEGER NOT NULL CHECK (quantity > 0),
|
|
||||||
price_cents INTEGER NOT NULL CHECK (price_cents >= 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_products_category ON products(category_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_orders_user ON orders(user_id);
|
|
||||||
`);
|
|
||||||
|
|
||||||
function formatPrice(cents) {
|
function formatPrice(cents) {
|
||||||
return (cents / 100).toLocaleString('ru-RU', {
|
return (cents / 100).toLocaleString('ru-RU', {
|
||||||
@@ -73,4 +43,10 @@ function formatPrice(cents) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { db, formatPrice, dbPath };
|
module.exports = {
|
||||||
|
pool,
|
||||||
|
query,
|
||||||
|
initSchema,
|
||||||
|
checkConnection,
|
||||||
|
formatPrice,
|
||||||
|
};
|
||||||
|
|||||||
+10
-7
@@ -1,3 +1,6 @@
|
|||||||
|
const { query } = require('../db');
|
||||||
|
const { asyncHandler } = require('../utils/asyncHandler');
|
||||||
|
|
||||||
function requireAuth(req, res, next) {
|
function requireAuth(req, res, next) {
|
||||||
if (!req.session.userId) {
|
if (!req.session.userId) {
|
||||||
const nextUrl = encodeURIComponent(req.originalUrl);
|
const nextUrl = encodeURIComponent(req.originalUrl);
|
||||||
@@ -6,17 +9,17 @@ function requireAuth(req, res, next) {
|
|||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadUser(req, res, next) {
|
const loadUser = asyncHandler(async (req, res, next) => {
|
||||||
if (req.session.userId) {
|
if (req.session.userId) {
|
||||||
const { db } = require('../db');
|
const { rows } = await query(
|
||||||
const user = db
|
'SELECT id, email, name FROM users WHERE id = $1',
|
||||||
.prepare('SELECT id, email, name FROM users WHERE id = ?')
|
[req.session.userId]
|
||||||
.get(req.session.userId);
|
);
|
||||||
res.locals.user = user || null;
|
res.locals.user = rows[0] || null;
|
||||||
} else {
|
} else {
|
||||||
res.locals.user = null;
|
res.locals.user = null;
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
}
|
});
|
||||||
|
|
||||||
module.exports = { requireAuth, loadUser };
|
module.exports = { requireAuth, loadUser };
|
||||||
|
|||||||
+43
-23
@@ -1,15 +1,16 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const { db } = require('../db');
|
const { query, formatPrice } = require('../db');
|
||||||
const { getCart, cartCount } = require('../cart');
|
const { getCart, cartCount } = require('../cart');
|
||||||
const { requireAuth } = require('../middleware/auth');
|
const { requireAuth } = require('../middleware/auth');
|
||||||
|
const { asyncHandler } = require('../utils/asyncHandler');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.use((req, res, next) => {
|
router.use((req, res, next) => {
|
||||||
const cart = getCart(req);
|
const cart = getCart(req);
|
||||||
res.locals.cartCount = cartCount(cart);
|
res.locals.cartCount = cartCount(cart);
|
||||||
res.locals.formatPrice = require('../db').formatPrice;
|
res.locals.formatPrice = formatPrice;
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -18,7 +19,9 @@ router.get('/register', (req, res) => {
|
|||||||
res.render('register', { title: 'Регистрация', error: null, values: {} });
|
res.render('register', { title: 'Регистрация', error: null, values: {} });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/register', (req, res) => {
|
router.post(
|
||||||
|
'/register',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
const { name, email, password, password2 } = req.body;
|
const { name, email, password, password2 } = req.body;
|
||||||
const values = { name, email };
|
const values = { name, email };
|
||||||
|
|
||||||
@@ -46,13 +49,14 @@ router.post('/register', (req, res) => {
|
|||||||
|
|
||||||
const hash = bcrypt.hashSync(password, 10);
|
const hash = bcrypt.hashSync(password, 10);
|
||||||
try {
|
try {
|
||||||
const r = db
|
const { rows } = await query(
|
||||||
.prepare('INSERT INTO users (email, password_hash, name) VALUES (?, ?, ?)')
|
'INSERT INTO users (email, password_hash, name) VALUES ($1, $2, $3) RETURNING id',
|
||||||
.run(email.trim().toLowerCase(), hash, name.trim());
|
[email.trim().toLowerCase(), hash, name.trim()]
|
||||||
req.session.userId = r.lastInsertRowid;
|
);
|
||||||
|
req.session.userId = rows[0].id;
|
||||||
res.redirect('/');
|
res.redirect('/');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
if (err.code === '23505') {
|
||||||
return res.status(400).render('register', {
|
return res.status(400).render('register', {
|
||||||
title: 'Регистрация',
|
title: 'Регистрация',
|
||||||
error: 'Этот email уже зарегистрирован',
|
error: 'Этот email уже зарегистрирован',
|
||||||
@@ -61,7 +65,8 @@ router.post('/register', (req, res) => {
|
|||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
router.get('/login', (req, res) => {
|
router.get('/login', (req, res) => {
|
||||||
if (req.session.userId) return res.redirect('/account');
|
if (req.session.userId) return res.redirect('/account');
|
||||||
@@ -73,14 +78,17 @@ router.get('/login', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/login', (req, res) => {
|
router.post(
|
||||||
|
'/login',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
const { email, password } = req.body;
|
const { email, password } = req.body;
|
||||||
const next = req.body.next || '/';
|
const next = req.body.next || '/';
|
||||||
const values = { email };
|
const values = { email };
|
||||||
|
|
||||||
const user = db
|
const { rows } = await query('SELECT * FROM users WHERE email = $1', [
|
||||||
.prepare('SELECT * FROM users WHERE email = ?')
|
(email || '').trim().toLowerCase(),
|
||||||
.get((email || '').trim().toLowerCase());
|
]);
|
||||||
|
const user = rows[0];
|
||||||
|
|
||||||
if (!user || !bcrypt.compareSync(password || '', user.password_hash)) {
|
if (!user || !bcrypt.compareSync(password || '', user.password_hash)) {
|
||||||
return res.status(401).render('login', {
|
return res.status(401).render('login', {
|
||||||
@@ -93,7 +101,8 @@ router.post('/login', (req, res) => {
|
|||||||
|
|
||||||
req.session.userId = user.id;
|
req.session.userId = user.id;
|
||||||
res.redirect(next.startsWith('/') ? next : '/');
|
res.redirect(next.startsWith('/') ? next : '/');
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
router.post('/logout', (req, res) => {
|
router.post('/logout', (req, res) => {
|
||||||
req.session.destroy(() => {
|
req.session.destroy(() => {
|
||||||
@@ -101,16 +110,27 @@ router.post('/logout', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/account', requireAuth, (req, res) => {
|
router.get(
|
||||||
const user = db
|
'/account',
|
||||||
.prepare('SELECT id, email, name, created_at FROM users WHERE id = ?')
|
requireAuth,
|
||||||
.get(req.session.userId);
|
asyncHandler(async (req, res) => {
|
||||||
|
const { rows } = await query(
|
||||||
|
'SELECT id, email, name, created_at FROM users WHERE id = $1',
|
||||||
|
[req.session.userId]
|
||||||
|
);
|
||||||
|
const user = rows[0];
|
||||||
|
|
||||||
const orderCount = db
|
const countResult = await query(
|
||||||
.prepare('SELECT COUNT(*) AS n FROM orders WHERE user_id = ?')
|
'SELECT COUNT(*)::int AS n FROM orders WHERE user_id = $1',
|
||||||
.get(user.id).n;
|
[user.id]
|
||||||
|
);
|
||||||
|
|
||||||
res.render('account', { title: 'Личный кабинет', user, orderCount });
|
res.render('account', {
|
||||||
});
|
title: 'Личный кабинет',
|
||||||
|
user,
|
||||||
|
orderCount: countResult.rows[0].n,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { db } = require('../db');
|
const { checkConnection } = require('../db');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get('/health', (_req, res) => {
|
router.get('/health', async (_req, res) => {
|
||||||
try {
|
try {
|
||||||
db.prepare('SELECT 1').get();
|
await checkConnection();
|
||||||
res.json({ ok: true, service: 'shop' });
|
res.json({ ok: true, service: 'shop', database: 'postgresql' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(503).json({ ok: false, error: err.message });
|
res.status(503).json({ ok: false, error: err.message });
|
||||||
}
|
}
|
||||||
|
|||||||
+121
-88
@@ -1,7 +1,8 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { db, formatPrice } = require('../db');
|
const { query, pool, formatPrice } = require('../db');
|
||||||
const { getCart, cartCount, cartItems, cartTotal } = require('../cart');
|
const { getCart, cartCount, cartItems, cartTotal } = require('../cart');
|
||||||
const { requireAuth } = require('../middleware/auth');
|
const { requireAuth } = require('../middleware/auth');
|
||||||
|
const { asyncHandler } = require('../utils/asyncHandler');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -16,7 +17,9 @@ router.use((req, res, next) => {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/', (req, res) => {
|
router.get(
|
||||||
|
'/',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
const category = req.query.category || '';
|
const category = req.query.category || '';
|
||||||
const q = (req.query.q || '').trim();
|
const q = (req.query.q || '').trim();
|
||||||
|
|
||||||
@@ -27,20 +30,21 @@ router.get('/', (req, res) => {
|
|||||||
WHERE p.stock > 0
|
WHERE p.stock > 0
|
||||||
`;
|
`;
|
||||||
const params = [];
|
const params = [];
|
||||||
|
let n = 1;
|
||||||
|
|
||||||
if (category) {
|
if (category) {
|
||||||
sql += ' AND c.slug = ?';
|
sql += ` AND c.slug = $${n++}`;
|
||||||
params.push(category);
|
params.push(category);
|
||||||
}
|
}
|
||||||
if (q) {
|
if (q) {
|
||||||
sql += ' AND (p.name LIKE ? OR p.description LIKE ?)';
|
sql += ` AND (p.name ILIKE $${n} OR p.description ILIKE $${n})`;
|
||||||
const like = `%${q}%`;
|
params.push(`%${q}%`);
|
||||||
params.push(like, like);
|
n++;
|
||||||
}
|
}
|
||||||
sql += ' ORDER BY p.name';
|
sql += ' ORDER BY p.name';
|
||||||
|
|
||||||
const products = db.prepare(sql).all(...params);
|
const { rows: products } = await query(sql, params);
|
||||||
const categories = db.prepare('SELECT * FROM categories ORDER BY name').all();
|
const { rows: categories } = await query('SELECT * FROM categories ORDER BY name');
|
||||||
|
|
||||||
res.render('home', {
|
res.render('home', {
|
||||||
title: 'Каталог',
|
title: 'Каталог',
|
||||||
@@ -49,17 +53,20 @@ router.get('/', (req, res) => {
|
|||||||
activeCategory: category,
|
activeCategory: category,
|
||||||
searchQuery: q,
|
searchQuery: q,
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
router.get('/product/:slug', (req, res) => {
|
router.get(
|
||||||
const product = db
|
'/product/:slug',
|
||||||
.prepare(
|
asyncHandler(async (req, res) => {
|
||||||
|
const { rows } = await query(
|
||||||
`SELECT p.*, c.name AS category_name, c.slug AS category_slug
|
`SELECT p.*, c.name AS category_name, c.slug AS category_slug
|
||||||
FROM products p
|
FROM products p
|
||||||
LEFT JOIN categories c ON c.id = p.category_id
|
LEFT JOIN categories c ON c.id = p.category_id
|
||||||
WHERE p.slug = ?`
|
WHERE p.slug = $1`,
|
||||||
)
|
[req.params.slug]
|
||||||
.get(req.params.slug);
|
);
|
||||||
|
const product = rows[0];
|
||||||
|
|
||||||
if (!product) {
|
if (!product) {
|
||||||
return res.status(404).render('error', {
|
return res.status(404).render('error', {
|
||||||
@@ -70,43 +77,51 @@ router.get('/product/:slug', (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.render('product', { title: product.name, product });
|
res.render('product', { title: product.name, product });
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
router.get('/cart', (req, res) => {
|
router.get(
|
||||||
|
'/cart',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
const cart = getCart(req);
|
const cart = getCart(req);
|
||||||
const items = cartItems(db, cart);
|
const items = await cartItems(cart);
|
||||||
const total = cartTotal(items);
|
const total = cartTotal(items);
|
||||||
|
|
||||||
const errorMsg = req.query.error ? decodeURIComponent(String(req.query.error)) : null;
|
const errorMsg = req.query.error ? decodeURIComponent(String(req.query.error)) : null;
|
||||||
|
|
||||||
res.render('cart', {
|
res.render('cart', {
|
||||||
title: 'Корзина',
|
title: 'Корзина',
|
||||||
items,
|
items,
|
||||||
total,
|
total,
|
||||||
error: errorMsg,
|
error: errorMsg,
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
router.post('/cart/add', (req, res) => {
|
router.post(
|
||||||
|
'/cart/add',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
const productId = parseInt(req.body.product_id, 10);
|
const productId = parseInt(req.body.product_id, 10);
|
||||||
const quantity = Math.max(1, parseInt(req.body.quantity, 10) || 1);
|
const quantity = Math.max(1, parseInt(req.body.quantity, 10) || 1);
|
||||||
|
|
||||||
const product = db
|
const { rows } = await query('SELECT id, stock FROM products WHERE id = $1', [
|
||||||
.prepare('SELECT id, stock FROM products WHERE id = ?')
|
productId,
|
||||||
.get(productId);
|
]);
|
||||||
|
const product = rows[0];
|
||||||
if (!product) {
|
if (!product) {
|
||||||
return res.redirect('/');
|
return res.redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
const cart = getCart(req);
|
const cart = getCart(req);
|
||||||
const current = cart[productId] || 0;
|
const current = cart[productId] || 0;
|
||||||
const nextQty = Math.min(product.stock, current + quantity);
|
cart[productId] = Math.min(product.stock, current + quantity);
|
||||||
cart[productId] = nextQty;
|
|
||||||
|
|
||||||
const redirect = req.body.redirect || '/cart';
|
res.redirect(req.body.redirect || '/cart');
|
||||||
res.redirect(redirect);
|
})
|
||||||
});
|
);
|
||||||
|
|
||||||
router.post('/cart/update', (req, res) => {
|
router.post(
|
||||||
|
'/cart/update',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
const cart = getCart(req);
|
const cart = getCart(req);
|
||||||
const updates = req.body.items || {};
|
const updates = req.body.items || {};
|
||||||
|
|
||||||
@@ -120,16 +135,17 @@ router.post('/cart/update', (req, res) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const product = db
|
const { rows } = await query('SELECT stock FROM products WHERE id = $1', [
|
||||||
.prepare('SELECT stock FROM products WHERE id = ?')
|
productId,
|
||||||
.get(productId);
|
]);
|
||||||
if (product) {
|
if (rows[0]) {
|
||||||
cart[productId] = Math.min(product.stock, quantity);
|
cart[productId] = Math.min(rows[0].stock, quantity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.redirect('/cart');
|
res.redirect('/cart');
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
router.post('/cart/remove/:id', (req, res) => {
|
router.post('/cart/remove/:id', (req, res) => {
|
||||||
const cart = getCart(req);
|
const cart = getCart(req);
|
||||||
@@ -137,9 +153,12 @@ router.post('/cart/remove/:id', (req, res) => {
|
|||||||
res.redirect('/cart');
|
res.redirect('/cart');
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/checkout', requireAuth, (req, res) => {
|
router.get(
|
||||||
|
'/checkout',
|
||||||
|
requireAuth,
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
const cart = getCart(req);
|
const cart = getCart(req);
|
||||||
const items = cartItems(db, cart);
|
const items = await cartItems(cart);
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return res.redirect('/cart');
|
return res.redirect('/cart');
|
||||||
}
|
}
|
||||||
@@ -150,11 +169,15 @@ router.get('/checkout', requireAuth, (req, res) => {
|
|||||||
total: cartTotal(items),
|
total: cartTotal(items),
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
router.post('/checkout', requireAuth, (req, res) => {
|
router.post(
|
||||||
|
'/checkout',
|
||||||
|
requireAuth,
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
const cart = getCart(req);
|
const cart = getCart(req);
|
||||||
const items = cartItems(db, cart);
|
const items = await cartItems(cart);
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return res.redirect('/cart');
|
return res.redirect('/cart');
|
||||||
}
|
}
|
||||||
@@ -170,74 +193,84 @@ router.post('/checkout', requireAuth, (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const total = cartTotal(items);
|
const total = cartTotal(items);
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
|
||||||
const placeOrder = db.transaction(() => {
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const row = db
|
const { rows } = await client.query(
|
||||||
.prepare('SELECT stock FROM products WHERE id = ?')
|
'SELECT stock FROM products WHERE id = $1 FOR UPDATE',
|
||||||
.get(item.id);
|
[item.id]
|
||||||
if (!row || row.stock < item.quantity) {
|
);
|
||||||
|
if (!rows[0] || rows[0].stock < item.quantity) {
|
||||||
throw new Error(`Недостаточно «${item.name}» на складе`);
|
throw new Error(`Недостаточно «${item.name}» на складе`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const order = db
|
const orderResult = await client.query(
|
||||||
.prepare(
|
|
||||||
`INSERT INTO orders (user_id, status, total_cents, customer_name, customer_email, customer_phone, address)
|
`INSERT INTO orders (user_id, status, total_cents, customer_name, customer_email, customer_phone, address)
|
||||||
VALUES (?, 'pending', ?, ?, ?, ?, ?)`
|
VALUES ($1, 'pending', $2, $3, $4, $5, $6)
|
||||||
)
|
RETURNING id`,
|
||||||
.run(
|
[
|
||||||
req.session.userId,
|
req.session.userId,
|
||||||
total,
|
total,
|
||||||
name.trim(),
|
name.trim(),
|
||||||
email.trim(),
|
email.trim(),
|
||||||
(phone || '').trim(),
|
(phone || '').trim(),
|
||||||
address.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 = ?'
|
|
||||||
);
|
);
|
||||||
|
const orderId = orderResult.rows[0].id;
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
insertItem.run(order.lastInsertRowid, item.id, item.quantity, item.price_cents);
|
await client.query(
|
||||||
updateStock.run(item.quantity, item.id);
|
`INSERT INTO order_items (order_id, product_id, quantity, price_cents)
|
||||||
|
VALUES ($1, $2, $3, $4)`,
|
||||||
|
[orderId, item.id, item.quantity, item.price_cents]
|
||||||
|
);
|
||||||
|
await client.query('UPDATE products SET stock = stock - $1 WHERE id = $2', [
|
||||||
|
item.quantity,
|
||||||
|
item.id,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return order.lastInsertRowid;
|
await client.query('COMMIT');
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const orderId = placeOrder();
|
|
||||||
req.session.cart = {};
|
req.session.cart = {};
|
||||||
res.redirect(`/orders/${orderId}?success=1`);
|
res.redirect(`/orders/${orderId}?success=1`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
res.redirect(`/cart?error=${encodeURIComponent(err.message)}`);
|
res.redirect(`/cart?error=${encodeURIComponent(err.message)}`);
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
router.get('/orders', requireAuth, (req, res) => {
|
router.get(
|
||||||
const orders = db
|
'/orders',
|
||||||
.prepare(
|
requireAuth,
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
|
const { rows: orders } = await query(
|
||||||
`SELECT id, status, total_cents, created_at
|
`SELECT id, status, total_cents, created_at
|
||||||
FROM orders WHERE user_id = ?
|
FROM orders WHERE user_id = $1
|
||||||
ORDER BY created_at DESC`
|
ORDER BY created_at DESC`,
|
||||||
)
|
[req.session.userId]
|
||||||
.all(req.session.userId);
|
);
|
||||||
|
|
||||||
res.render('orders', { title: 'Мои заказы', orders });
|
res.render('orders', { title: 'Мои заказы', orders });
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
router.get('/orders/:id', requireAuth, (req, res) => {
|
router.get(
|
||||||
const order = db
|
'/orders/:id',
|
||||||
.prepare(
|
requireAuth,
|
||||||
`SELECT * FROM orders WHERE id = ? AND user_id = ?`
|
asyncHandler(async (req, res) => {
|
||||||
)
|
const { rows } = await query(
|
||||||
.get(req.params.id, req.session.userId);
|
'SELECT * FROM orders WHERE id = $1 AND user_id = $2',
|
||||||
|
[req.params.id, req.session.userId]
|
||||||
|
);
|
||||||
|
const order = rows[0];
|
||||||
|
|
||||||
if (!order) {
|
if (!order) {
|
||||||
return res.status(404).render('error', {
|
return res.status(404).render('error', {
|
||||||
@@ -247,14 +280,13 @@ router.get('/orders/:id', requireAuth, (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = db
|
const { rows: items } = await query(
|
||||||
.prepare(
|
|
||||||
`SELECT oi.*, p.name, p.slug, p.image_url
|
`SELECT oi.*, p.name, p.slug, p.image_url
|
||||||
FROM order_items oi
|
FROM order_items oi
|
||||||
JOIN products p ON p.id = oi.product_id
|
JOIN products p ON p.id = oi.product_id
|
||||||
WHERE oi.order_id = ?`
|
WHERE oi.order_id = $1`,
|
||||||
)
|
[order.id]
|
||||||
.all(order.id);
|
);
|
||||||
|
|
||||||
res.render('order', {
|
res.render('order', {
|
||||||
title: `Заказ #${order.id}`,
|
title: `Заказ #${order.id}`,
|
||||||
@@ -262,6 +294,7 @@ router.get('/orders/:id', requireAuth, (req, res) => {
|
|||||||
items,
|
items,
|
||||||
success: req.query.success === '1',
|
success: req.query.success === '1',
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
+24
-21
@@ -1,20 +1,12 @@
|
|||||||
const { db } = require('./db');
|
const { query } = require('./db');
|
||||||
|
|
||||||
function runSeed() {
|
async function runSeed() {
|
||||||
const count = db.prepare('SELECT COUNT(*) AS n FROM products').get().n;
|
const { rows } = await query('SELECT COUNT(*)::int AS n FROM products');
|
||||||
if (count > 0) {
|
if (rows[0].n > 0) {
|
||||||
console.log('База уже содержит товары, пропуск seed.');
|
console.log('База уже содержит товары, пропуск seed.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertCategory = db.prepare(
|
|
||||||
'INSERT INTO categories (slug, name) VALUES (?, ?)'
|
|
||||||
);
|
|
||||||
const insertProduct = db.prepare(`
|
|
||||||
INSERT INTO products (category_id, slug, name, description, price_cents, stock, image_url)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
||||||
`);
|
|
||||||
|
|
||||||
const categories = [
|
const categories = [
|
||||||
{ slug: 'electronics', name: 'Электроника' },
|
{ slug: 'electronics', name: 'Электроника' },
|
||||||
{ slug: 'clothing', name: 'Одежда' },
|
{ slug: 'clothing', name: 'Одежда' },
|
||||||
@@ -22,10 +14,12 @@ function runSeed() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const categoryIds = {};
|
const categoryIds = {};
|
||||||
const seed = db.transaction(() => {
|
|
||||||
for (const c of categories) {
|
for (const c of categories) {
|
||||||
const r = insertCategory.run(c.slug, c.name);
|
const r = await query(
|
||||||
categoryIds[c.slug] = r.lastInsertRowid;
|
'INSERT INTO categories (slug, name) VALUES ($1, $2) RETURNING id',
|
||||||
|
[c.slug, c.name]
|
||||||
|
);
|
||||||
|
categoryIds[c.slug] = r.rows[0].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const products = [
|
const products = [
|
||||||
@@ -112,24 +106,33 @@ function runSeed() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (const p of products) {
|
for (const p of products) {
|
||||||
insertProduct.run(
|
await query(
|
||||||
|
`INSERT INTO products (category_id, slug, name, description, price_cents, stock, image_url)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||||
|
[
|
||||||
categoryIds[p.cat],
|
categoryIds[p.cat],
|
||||||
p.slug,
|
p.slug,
|
||||||
p.name,
|
p.name,
|
||||||
p.description,
|
p.description,
|
||||||
p.price,
|
p.price,
|
||||||
p.stock,
|
p.stock,
|
||||||
p.image
|
p.image,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
seed();
|
console.log('Добавлено категорий:', categories.length, ', товаров:', products.length);
|
||||||
console.log('Добавлено категорий:', categories.length, ', товаров: 8');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { runSeed };
|
module.exports = { runSeed };
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
runSeed();
|
const { initSchema, pool } = require('./db');
|
||||||
|
initSchema()
|
||||||
|
.then(() => runSeed())
|
||||||
|
.then(() => pool.end())
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-28
@@ -1,36 +1,44 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const session = require('express-session');
|
const session = require('express-session');
|
||||||
const SQLiteStore = require('connect-sqlite3')(session);
|
const pgSession = require('connect-pg-simple')(session);
|
||||||
|
|
||||||
require('./db');
|
|
||||||
require('./seed').runSeed();
|
|
||||||
|
|
||||||
|
const { pool, initSchema, checkConnection } = require('./db');
|
||||||
|
const { runSeed } = require('./seed');
|
||||||
const { loadUser } = require('./middleware/auth');
|
const { loadUser } = require('./middleware/auth');
|
||||||
const healthRoutes = require('./routes/health');
|
const healthRoutes = require('./routes/health');
|
||||||
const shopRoutes = require('./routes/shop');
|
const shopRoutes = require('./routes/shop');
|
||||||
const authRoutes = require('./routes/auth');
|
const authRoutes = require('./routes/auth');
|
||||||
|
|
||||||
const app = express();
|
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
const HOST = process.env.HOST || '0.0.0.0';
|
const HOST = process.env.HOST || '0.0.0.0';
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
if (process.env.TRUST_PROXY === '1' || isProduction) {
|
async function start() {
|
||||||
|
await checkConnection();
|
||||||
|
await initSchema();
|
||||||
|
await runSeed();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
if (process.env.TRUST_PROXY === '1' || isProduction) {
|
||||||
app.set('trust proxy', 1);
|
app.set('trust proxy', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.set('view engine', 'ejs');
|
app.set('view engine', 'ejs');
|
||||||
app.set('views', path.join(__dirname, 'views'));
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
|
||||||
app.use(healthRoutes);
|
app.use(healthRoutes);
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
app.use(
|
||||||
app.use(express.urlencoded({ extended: true }));
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
session({
|
session({
|
||||||
store: new SQLiteStore({ db: 'sessions.db', dir: path.join(__dirname, '..', 'data') }),
|
store: new pgSession({
|
||||||
|
pool,
|
||||||
|
createTableIfMissing: true,
|
||||||
|
tableName: 'session',
|
||||||
|
}),
|
||||||
secret: process.env.SESSION_SECRET || 'dev-secret-change-in-production',
|
secret: process.env.SESSION_SECRET || 'dev-secret-change-in-production',
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
@@ -41,35 +49,40 @@ app.use(
|
|||||||
secure: isProduction,
|
secure: isProduction,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
app.use(loadUser);
|
app.use(loadUser);
|
||||||
|
app.use('/', shopRoutes);
|
||||||
|
app.use('/', authRoutes);
|
||||||
|
|
||||||
app.use('/', shopRoutes);
|
app.use((req, res) => {
|
||||||
app.use('/', authRoutes);
|
|
||||||
|
|
||||||
app.use((req, res) => {
|
|
||||||
res.status(404).render('error', {
|
res.status(404).render('error', {
|
||||||
title: 'Не найдено',
|
title: 'Не найдено',
|
||||||
message: 'Страница не найдена',
|
message: 'Страница не найдена',
|
||||||
code: 404,
|
code: 404,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use((err, req, res, _next) => {
|
app.use((err, req, res, _next) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).render('error', {
|
res.status(500).render('error', {
|
||||||
title: 'Ошибка',
|
title: 'Ошибка',
|
||||||
message: 'Внутренняя ошибка сервера',
|
message: 'Внутренняя ошибка сервера',
|
||||||
code: 500,
|
code: 500,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const server = app.listen(PORT, HOST, () => {
|
const server = app.listen(PORT, HOST, () => {
|
||||||
console.log(`Магазин: http://${HOST}:${PORT}`);
|
console.log(`Магазин: http://${HOST}:${PORT} (PostgreSQL)`);
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on('error', (err) => {
|
server.on('error', (err) => {
|
||||||
console.error('Не удалось запустить сервер:', err.message);
|
console.error('Не удалось запустить сервер:', err.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
start().catch((err) => {
|
||||||
|
console.error('Ошибка запуска:', err.message);
|
||||||
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
function asyncHandler(fn) {
|
||||||
|
return (req, res, next) => {
|
||||||
|
Promise.resolve(fn(req, res, next)).catch(next);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { asyncHandler };
|
||||||
Reference in New Issue
Block a user