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 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 23:29:06 +03:00
parent 68c17b16fe
commit e0247bc288
9 changed files with 231 additions and 16 deletions

71
README.md Normal file
View File

@@ -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`.
**Обязательно смените пароль** через интерфейс или напрямую в БД.

View File

@@ -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",

20
deploy.sh Normal file
View File

@@ -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"

3
server/.env.example Normal file
View File

@@ -0,0 +1,3 @@
PORT=3040
JWT_SECRET=заменитеа_случайную_строку_32_символа
ALLOWED_ORIGIN=http://cats.heagbokat-server.ru

View File

@@ -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",

View File

@@ -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",

View File

@@ -41,7 +41,8 @@ export async function initDb(): Promise<SqlJsDatabase> {
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(`

View File

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

View File

@@ -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;