From e0247bc2887ad57412d2b417d7177210d4d62253 Mon Sep 17 00:00:00 2001 From: heagbokat Date: Thu, 25 Jun 2026 23:29:06 +0300 Subject: [PATCH] Fix case-insensitive auth and harden for production - Fix login/register to treat usernames case-insensitively (HeagBoKaT == heagbokat) using LOWER() in SQL with JS .toLowerCase() for Unicode safety - Add helmet for HTTP security headers and express-rate-limit on auth endpoints (20 req / 15 min) to prevent brute-force - Lock CORS to ALLOWED_ORIGIN env var; warn if JWT_SECRET is the default dev key - Serve built client/dist from Express so a single PM2 process handles everything - Add deploy.sh, README.md with full first-run and auto-restart instructions, and server/.env.example Co-Authored-By: Claude Sonnet 4.6 --- README.md | 71 +++++++++++++++++++++++++++++++++++++++ client/package-lock.json | 56 ++++++++++++++++++++++++------ deploy.sh | 20 +++++++++++ server/.env.example | 3 ++ server/package-lock.json | 52 ++++++++++++++++++++++++++++ server/package.json | 3 ++ server/src/db.ts | 3 +- server/src/index.ts | 33 +++++++++++++++++- server/src/routes/auth.ts | 6 ++-- 9 files changed, 231 insertions(+), 16 deletions(-) create mode 100644 README.md create mode 100644 deploy.sh create mode 100644 server/.env.example diff --git a/README.md b/README.md new file mode 100644 index 0000000..6c55378 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# Котограм + +Сайт для публикации фотографий котов. React + Vite (фронт), Express + TypeScript (бэк), SQLite. + +## Первый запуск на сервере + +```bash +cd /var/www/cats + +# 1. Установить зависимости и собрать фронт +npm install --prefix server +npm install --prefix client +npm run build + +# 2. Создать файл переменных окружения +cp server/.env.example server/.env +# Отредактировать server/.env: задать JWT_SECRET и ALLOWED_ORIGIN +nano server/.env + +# 3. Запустить через PM2 +sudo pm2 start npm --name cats -- start + +# 4. Зарегистрировать автостарт (один раз на машине) +sudo pm2 startup +# Скопировать и выполнить команду, которую выведет pm2 startup +sudo pm2 save +``` + +## Переменные окружения (`server/.env`) + +```env +PORT=3040 +JWT_SECRET=замените_на_случайную_строку_32_символа +ALLOWED_ORIGIN=http://cats.heagbokat-server.ru +``` + +Сгенерировать JWT_SECRET: +```bash +node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" +``` + +## Деплой обновлений + +```bash +cd /var/www/cats +chmod +x deploy.sh # только первый раз +./deploy.sh +``` + +Скрипт делает: `git pull` → сборка фронта → перезапуск PM2. + +## Управление PM2 + +```bash +sudo pm2 ps # статус процессов +sudo pm2 logs cats # логи сервера +sudo pm2 restart cats # перезапустить +sudo pm2 stop cats # остановить +``` + +## Данные (не трогать!) + +- `server/data.db` — база данных (пользователи, лайки, коты) +- `server/uploads/` — загруженные изображения + +Оба пути в `.gitignore` — `git pull` их не перезапишет. + +## Первый администратор + +При первом запуске автоматически создаётся пользователь `admin / admin123`. +**Обязательно смените пароль** через интерфейс или напрямую в БД. diff --git a/client/package-lock.json b/client/package-lock.json index 53730ef..3f4de46 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -25,6 +25,7 @@ "tailwindcss-animate": "^1.0.7" }, "devDependencies": { + "@tailwindcss/postcss": "^4.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.0", @@ -39,6 +40,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -78,7 +80,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -814,6 +815,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -824,6 +826,7 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -834,6 +837,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -843,12 +847,14 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2353,6 +2359,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", @@ -2368,6 +2375,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 20" @@ -2394,6 +2402,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2410,6 +2419,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2426,6 +2436,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2442,6 +2453,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2458,6 +2470,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2474,6 +2487,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2490,6 +2504,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2506,6 +2521,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2522,6 +2538,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2546,6 +2563,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2567,6 +2585,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2583,6 +2602,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2596,6 +2616,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.0.tgz", "integrity": "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==", + "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -2689,7 +2710,6 @@ "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2700,7 +2720,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2847,7 +2866,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -2986,6 +3004,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -3022,6 +3041,7 @@ "version": "5.22.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz", "integrity": "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==", + "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -3304,6 +3324,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/has-symbols": { @@ -3362,6 +3383,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -3403,6 +3425,7 @@ "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -3435,6 +3458,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -3455,6 +3479,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -3475,6 +3500,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -3495,6 +3521,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -3515,6 +3542,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -3535,6 +3563,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -3555,6 +3584,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -3575,6 +3605,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -3595,6 +3626,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -3615,6 +3647,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -3635,6 +3668,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -3683,6 +3717,7 @@ "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -3728,6 +3763,7 @@ "version": "3.3.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, "funding": [ { "type": "github", @@ -3765,6 +3801,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -3773,7 +3810,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3785,6 +3821,7 @@ "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -3800,7 +3837,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -3842,7 +3878,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3852,7 +3887,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -4071,6 +4105,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -4090,8 +4125,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -4106,6 +4140,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4241,7 +4276,6 @@ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..fa061cb --- /dev/null +++ b/deploy.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +echo "==> Pulling latest changes..." +git pull + +echo "==> Installing server dependencies..." +npm install --prefix server + +echo "==> Installing client dependencies..." +npm install --prefix client + +echo "==> Building client..." +npm run build + +echo "==> Restarting PM2 process..." +sudo pm2 restart cats +sudo pm2 save + +echo "==> Done! Site updated at http://cats.heagbokat-server.ru" diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 0000000..e308e0c --- /dev/null +++ b/server/.env.example @@ -0,0 +1,3 @@ +PORT=3040 +JWT_SECRET=замените_на_случайную_строку_32_символа +ALLOWED_ORIGIN=http://cats.heagbokat-server.ru diff --git a/server/package-lock.json b/server/package-lock.json index b670abc..c23a48c 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -10,6 +10,8 @@ "cors": "^2.8.5", "dotenv": "^16.4.0", "express": "^4.21.0", + "express-rate-limit": "^8.5.2", + "helmet": "^8.2.0", "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.1", "sql.js": "^1.14.1" @@ -18,6 +20,7 @@ "@types/bcryptjs": "^2.4.6", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/express-rate-limit": "^5.1.3", "@types/jsonwebtoken": "^9.0.7", "@types/multer": "^1.4.12", "@types/node": "^22.0.0", @@ -518,6 +521,16 @@ "@types/serve-static": "^1" } }, + "node_modules/@types/express-rate-limit": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-rate-limit/-/express-rate-limit-5.1.3.tgz", + "integrity": "sha512-H+TYy3K53uPU2TqPGFYaiWc2xJV6+bIFkDd/Ma2/h67Pa6ARk9kWE0p/K9OH1Okm0et9Sfm66fmXoAxsH2PHXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/express-serve-static-core": { "version": "4.19.8", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", @@ -1031,6 +1044,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -1164,6 +1195,18 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.2.0.tgz", + "integrity": "sha512-DRgTIUgnWcJ62KyarxxziuqYxKGnR6Rgg19BlbucN/dpmJbl1XOit6qvoOX0ZT+HhWe5OUVhU/a1zpGyc1xA0Q==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/EvanHahn" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -1202,6 +1245,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", diff --git a/server/package.json b/server/package.json index 7c4cee1..14f10b8 100644 --- a/server/package.json +++ b/server/package.json @@ -11,6 +11,8 @@ "cors": "^2.8.5", "dotenv": "^16.4.0", "express": "^4.21.0", + "express-rate-limit": "^8.5.2", + "helmet": "^8.2.0", "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.1", "sql.js": "^1.14.1" @@ -19,6 +21,7 @@ "@types/bcryptjs": "^2.4.6", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/express-rate-limit": "^5.1.3", "@types/jsonwebtoken": "^9.0.7", "@types/multer": "^1.4.12", "@types/node": "^22.0.0", diff --git a/server/src/db.ts b/server/src/db.ts index ef869ab..6c96536 100644 --- a/server/src/db.ts +++ b/server/src/db.ts @@ -41,7 +41,8 @@ export async function initDb(): Promise { if (!admin) { const hash = bcrypt.hashSync('admin123', 10); execute('INSERT INTO users (username, password_hash, is_admin) VALUES (?, ?, 1)', ['admin', hash]); - console.log('Admin user created: admin / admin123'); + console.warn('⚠️ Admin user created with default password: admin / admin123'); + console.warn('⚠️ CHANGE THE ADMIN PASSWORD IMMEDIATELY!'); } db.run(` diff --git a/server/src/index.ts b/server/src/index.ts index ff2e1d4..0c68897 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,6 +1,8 @@ import 'dotenv/config'; import express from 'express'; import cors from 'cors'; +import helmet from 'helmet'; +import rateLimit from 'express-rate-limit'; import path from 'path'; import { fileURLToPath } from 'url'; import { initDb } from './db.js'; @@ -13,15 +15,37 @@ import userRoutes from './routes/users.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const DEV_JWT_SECRET = 'catstagram-dev-secret-key-change-in-production'; +if (!process.env.JWT_SECRET || process.env.JWT_SECRET === DEV_JWT_SECRET) { + console.warn('⚠️ WARNING: JWT_SECRET not set or using default dev key. Set a strong secret in server/.env for production!'); +} + async function main() { await initDb(); const app = express(); const PORT = parseInt(process.env.PORT || '3040'); - app.use(cors()); + app.use(helmet({ + crossOriginEmbedderPolicy: false, + contentSecurityPolicy: false, + })); + + const allowedOrigin = process.env.ALLOWED_ORIGIN || `http://localhost:${PORT}`; + app.use(cors({ origin: allowedOrigin, credentials: true })); + app.use(express.json()); + const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 20, + message: { error: 'Слишком много попыток, подождите 15 минут' }, + standardHeaders: true, + legacyHeaders: false, + }); + app.use('/api/auth/login', authLimiter); + app.use('/api/auth/register', authLimiter); + app.use('/uploads', express.static(path.join(__dirname, '..', 'uploads'))); app.use('/api/auth', authRoutes); @@ -35,6 +59,13 @@ async function main() { res.json({ status: 'ok' }); }); + // Serve built React client + const clientDist = path.join(__dirname, '..', '..', 'client', 'dist'); + app.use(express.static(clientDist)); + app.get('*', (_req, res) => { + res.sendFile(path.join(clientDist, 'index.html')); + }); + app.listen(PORT, () => { console.log(`Котограм server running on http://localhost:${PORT}`); }); diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index c66aaf8..e084398 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -53,7 +53,7 @@ router.post('/register', (req: Request, res: Response) => { return; } - const existing = queryOne('SELECT id FROM users WHERE username = ?', [username]); + const existing = queryOne('SELECT id FROM users WHERE LOWER(username) = ?', [username.toLowerCase()]); if (existing) { res.status(409).json({ error: 'Имя пользователя уже занято' }); return; @@ -62,7 +62,7 @@ router.post('/register', (req: Request, res: Response) => { const password_hash = bcrypt.hashSync(password, 10); execute('INSERT INTO users (username, password_hash) VALUES (?, ?)', [username, password_hash]); - const user = queryOne('SELECT id, username, points, is_admin, avatar_url FROM users WHERE username = ?', [username]); + const user = queryOne('SELECT id, username, points, is_admin, avatar_url FROM users WHERE LOWER(username) = ?', [username.toLowerCase()]); const token = generateToken(user.id, user.username, !!user.is_admin); res.status(201).json({ token, user: userToResponse(user) }); @@ -71,7 +71,7 @@ router.post('/register', (req: Request, res: Response) => { router.post('/login', (req: Request, res: Response) => { const { username, password } = req.body; - const user = queryOne('SELECT id, username, password_hash, points, is_admin, avatar_url FROM users WHERE username = ?', [username]); + const user = queryOne('SELECT id, username, password_hash, points, is_admin, avatar_url FROM users WHERE LOWER(username) = ?', [username.toLowerCase()]); if (!user || !bcrypt.compareSync(password, user.password_hash)) { res.status(401).json({ error: 'Неверное имя пользователя или пароль' }); return;