From e9aec9a65dc04c00b66ee0dea104dd8725e03e35 Mon Sep 17 00:00:00 2001 From: aodulov Date: Thu, 18 Dec 2025 13:03:12 +0200 Subject: [PATCH] NAS deployment fixed --- .env | 4 +- Dockerfile.node-apps | 12 +++ admin_check.js | 51 +++++++++++-- deployment_guide.md | 88 +++++++++++++++++++--- ecosystem.config.cjs | 4 +- package-lock.json | 8 +- server/.env | 22 ------ server/package.json | 16 ++-- server/prisma.config.ts | 4 +- server/prod.db | Bin 98304 -> 98304 bytes server/reset_prod_db.js | 80 ++++++++++++++++++++ server/src/controllers/auth.controller.ts | 6 ++ server/src/index.ts | 17 ++++- server/src/services/auth.service.ts | 42 +++++++---- 14 files changed, 278 insertions(+), 76 deletions(-) create mode 100644 Dockerfile.node-apps delete mode 100644 server/.env create mode 100644 server/reset_prod_db.js diff --git a/.env b/.env index 8cc8b49..b9ae856 100644 --- a/.env +++ b/.env @@ -12,8 +12,8 @@ ADMIN_PASSWORD_TEST="admin123" # PROD DATABASE_URL_PROD="file:./prisma/prod.db" -ADMIN_EMAIL_PROD="admin-prod@gymflow.ai" -ADMIN_PASSWORD_PROD="secure-prod-password-change-me" +ADMIN_EMAIL_PROD="ag@gymflow.ai" +ADMIN_PASSWORD_PROD="masterbladdercontrol12" # Fallback for Prisma CLI (Migrate default) DATABASE_URL="file:./prisma/dev.db" diff --git a/Dockerfile.node-apps b/Dockerfile.node-apps new file mode 100644 index 0000000..99134e6 --- /dev/null +++ b/Dockerfile.node-apps @@ -0,0 +1,12 @@ +FROM node:lts-slim + +# Install build dependencies for better-sqlite3 and other native modules +RUN apt-get update && apt-get install -y \ + python3 \ + build-essential \ + openssl \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /usr/src/app diff --git a/admin_check.js b/admin_check.js index 18e901d..c5c608e 100644 --- a/admin_check.js +++ b/admin_check.js @@ -1,13 +1,48 @@ -// Simple script to check for admin user const { PrismaClient } = require('@prisma/client'); -(async () => { - const prisma = new PrismaClient(); +const path = require('path'); +const fs = require('fs'); + +async function checkAdmin() { + process.env.APP_MODE = 'prod'; + + // Attempt to locate database path similar to production + let dbPath = './server/prod.db'; + if (!fs.existsSync(dbPath)) { + dbPath = './prod.db'; // If running from inside server dir + } + + console.log(`Checking database at: ${path.resolve(dbPath)}`); + console.log(`File exists: ${fs.existsSync(dbPath)}`); + + if (!fs.existsSync(dbPath)) { + console.error('CRITICAL: Database file not found! Ensure pm2 is running from the correct directory.'); + return; + } + + const prisma = new PrismaClient({ + datasources: { + db: { + url: `file:${path.resolve(dbPath)}`, + }, + }, + }); + try { - const admin = await prisma.user.findFirst({ where: { role: 'ADMIN' } }); - console.log('Admin user:', admin); - } catch (e) { - console.error('Error:', e); + const admin = await prisma.user.findFirst({ + where: { role: 'ADMIN' }, + }); + + if (admin) { + console.log(`✅ Admin user found: ${admin.email}`); + console.log(`First login: ${admin.isFirstLogin}`); + } else { + console.log('❌ No admin user found in database.'); + } + } catch (error) { + console.error('Error connecting to database:', error.message); } finally { await prisma.$disconnect(); } -})(); +} + +checkAdmin(); diff --git a/deployment_guide.md b/deployment_guide.md index 35a3509..47ad0e5 100644 --- a/deployment_guide.md +++ b/deployment_guide.md @@ -10,19 +10,29 @@ You need to build the application on your local machine before transferring it t Run the following in the project root: ```bash npm install +npm install npm run build ``` +> [!IMPORTANT] +> If you encounter a `TypeError: Missing parameter name` in your logs, ensure you have rebuilt the app with the latest changes (`app.get('*all', ...)`). This creates a `dist` folder with the compiled frontend. ### Build Backend -Run the following in the project root: +1. Update your `server/package.json` to have these scripts for production: +```json +"scripts": { + "start": "node dist/index.js", + "start:prod": "node dist/index.js", + ... +} +``` +2. Run the following locally: ```bash cd server npm install npm run build cd .. ``` -This creates a `server/dist` folder with the compiled backend. ## 2. Transfer Files @@ -38,22 +48,43 @@ Ensure the following files/folders are present on the NAS: *Note: You do not need to copy `node_modules`. We will install production dependencies on the NAS to ensure compatibility.* -## 3. Integration +Update your `docker-compose.yml` to use a custom Dockerfile (to support building native modules like `better-sqlite3`) and map the new port. -Update your `docker-compose.yml` to include GymFlow in the startup command and map the new port. +### 2a. Use Custom Dockerfile +The standard `node:lts-slim` image lacks Python and build tools. Use the provided [Dockerfile.node-apps](file:///d:/Coding/gymflow/Dockerfile.node-apps): + +1. Copy `Dockerfile.node-apps` to your NAS alongside `docker-compose.yml`. +2. Modify `docker-compose.yml`: +```yaml +services: + nodejs-apps: + build: + context: . + dockerfile: Dockerfile.node-apps + container_name: node-apps + # image: node:lts-slim <-- Comment this out + ... +``` + +### 3. Integration ### Update `command` -Add the following to your existing command string (append before the final `pm2 logs`): +Add the following to your existing command string. We added `npx prisma generate` explicitly to ensure the Linux client is built correctly, and we ensure `DATABASE_URL` is consistent: + ```bash -... && cd ../gymflow/server && npm install --omit=dev && DATABASE_URL=file:./prod.db npx prisma db push && cd .. && pm2 start ecosystem.config.cjs && ... +... && cd ../gymflow/server && npm install --omit=dev && APP_MODE=prod DATABASE_URL=file:./prod.db npx prisma generate && APP_MODE=prod DATABASE_URL=file:./prod.db npx prisma db push && cd .. && pm2 start ecosystem.config.cjs && ... ``` *Note: We assume `gymflow` is a sibling of `ag-beats` etc.* **Full Command Example:** ```yaml -command: /bin/sh -c "npm install -g pm2 && cd ag-beats && npm install && cd ../ball-shooting && npm install && cd ../gymflow/server && npm install --omit=dev && DATABASE_URL=file:./prod.db npx prisma db push && cd .. && pm2 start ecosystem.config.cjs && cd ../ag-beats && pm2 start ecosystem.config.js && cd ../ball-shooting && pm2 start npm --name ag-ball -- start && pm2 logs --raw" +command: /bin/sh -c "npm install -g pm2 && cd ag-beats && npm install && cd ../ball-shooting && npm install && cd ../gymflow/server && npm install --omit=dev && APP_MODE=prod DATABASE_URL=file:./prod.db npx prisma generate && APP_MODE=prod DATABASE_URL=file:./prod.db npx prisma db push && cd .. && pm2 start ecosystem.config.cjs && cd ../ag-beats && pm2 start ecosystem.config.js && cd ../ball-shooting && pm2 start npm --name ag-ball -- start && pm2 logs --raw" ``` +> [!IMPORTANT] +> Since we updated the `Dockerfile.node-apps` to include `openssl` (required by Prisma), you **must** rebuild the image: +> `docker-compose up -d --build nodejs-apps` + ### Update `ports` Add the GymFlow port (3003 inside container, mapped to your choice, e.g., 3033): ```yaml @@ -64,12 +95,45 @@ ports: - "3033:3003" # GymFlow ``` -## 4. Environment Variables +## 4. Unified Ecosystem Config +Your common `ecosystem.config.js` should look like this to ensure GymFlow has the correct production environment: -GymFlow uses `dotenv` but in this setup PM2 handles the variables via `ecosystem.config.cjs`. -- Port: `3003` (Internal) -- Database: `server/prod.db` (SQLite) -- Node Env: `production` +```javascript +module.exports = { + apps: [ + { + name: "ag-home", + script: "server.js", + cwd: "/usr/src/app", + exec_mode: "fork", + }, + { + name: "ag-beats", + script: "npm", + args: "start", + cwd: "/usr/src/app/ag-beats", + }, + { + name: "ag-ball", + script: "npm", + args: "start", + cwd: "/usr/src/app/ball-shooting", + }, + { + name: "gymflow", + script: "dist/index.js", + cwd: "/usr/src/app/gymflow/server", + env: { + NODE_ENV: "production", + APP_MODE: "prod", + PORT: 3003, + DATABASE_URL: "file:./prod.db", + DATABASE_URL_PROD: "file:./prod.db" + } + } + ] +}; +``` ## 5. Nginx Proxy Manager diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index 764728b..82959f1 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -11,8 +11,10 @@ module.exports = { max_memory_restart: '1G', env: { NODE_ENV: 'production', + APP_MODE: 'prod', PORT: 3003, - DATABASE_URL: 'file:../prod.db' // Relative to server/dist/index.js? No, relative to CWD if using prisma. + DATABASE_URL: 'file:./prod.db', // Consistent with the npx prisma db push command + DATABASE_URL_PROD: 'file:./prod.db', // Actually, prisma runs from where schema is or based on env. // Let's assume the DB is in the root or server dir. // In package.json: "start:prod": "cross-env APP_MODE=prod DATABASE_URL=file:./prod.db ... src/index.ts" diff --git a/package-lock.json b/package-lock.json index 2db7e56..8cf421f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4508,12 +4508,12 @@ } }, "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", "dependencies": { - "jwa": "^2.0.0", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, diff --git a/server/.env b/server/.env deleted file mode 100644 index 9086a20..0000000 --- a/server/.env +++ /dev/null @@ -1,22 +0,0 @@ -# Generic - -# DEV -DATABASE_URL_DEV="file:./prisma/dev.db" -ADMIN_EMAIL_DEV="admin@gymflow.ai" -ADMIN_PASSWORD_DEV="admin123" - -# TEST -DATABASE_URL_TEST="file:./prisma/test.db" -ADMIN_EMAIL_TEST="admin@gymflow.ai" -ADMIN_PASSWORD_TEST="admin123" - -# PROD -DATABASE_URL_PROD="file:./prisma/prod.db" -ADMIN_EMAIL_PROD="admin-prod@gymflow.ai" -ADMIN_PASSWORD_PROD="secure-prod-password-change-me" - -# Fallback for Prisma CLI (Migrate default) -DATABASE_URL="file:./prisma/dev.db" - -GEMINI_API_KEY=AIzaSyC88SeFyFYjvSfTqgvEyr7iqLSvEhuadoE -DEFAULT_EXERCISES_CSV_PATH="default_exercises.csv" diff --git a/server/package.json b/server/package.json index 56a9361..38438d8 100644 --- a/server/package.json +++ b/server/package.json @@ -4,17 +4,17 @@ "description": "Backend for GymFlow AI", "main": "src/index.ts", "scripts": { - "start": "npm run start:prod", - "start:prod": "cross-env APP_MODE=prod DATABASE_URL=file:./prod.db npx prisma db push && cross-env APP_MODE=prod DATABASE_URL_PROD=file:./prod.db ts-node-dev -r dotenv/config --respawn --transpile-only src/index.ts", - "start:test": "cross-env APP_MODE=test DATABASE_URL=file:./test.db DATABASE_URL_TEST=file:./test.db npx prisma db push --accept-data-loss && cross-env APP_MODE=test DATABASE_URL_TEST=file:./test.db ts-node-dev -r dotenv/config --respawn --transpile-only src/index.ts", - "dev": "cross-env APP_MODE=dev ts-node-dev -r dotenv/config --respawn --transpile-only src/index.ts", + "start": "node dist/index.js", + "start:prod": "node dist/index.js", + "start:dev": "cross-env APP_MODE=dev ts-node-dev -r dotenv/config --respawn --transpile-only src/index.ts", + "dev": "npm run start:dev", "build": "tsc", "migrate:deploy": "npx prisma migrate deploy" }, "dependencies": { "@google/generative-ai": "^0.24.1", - "@prisma/adapter-better-sqlite3": "^7.1.0", - "@prisma/client": "^7.1.0", + "@prisma/adapter-better-sqlite3": "^7.2.0", + "@prisma/client": "^7.2.0", "@types/better-sqlite3": "^7.6.13", "bcryptjs": "3.0.3", "better-sqlite3": "^11.0.0", @@ -22,11 +22,11 @@ "dotenv": "17.2.3", "express": "5.1.0", "jsonwebtoken": "9.0.2", - "ts-node-dev": "^2.0.0", "winston": "^3.19.0", "zod": "^4.1.13" }, "devDependencies": { + "ts-node-dev": "^2.0.0", "@types/bcryptjs": "*", "@types/cors": "*", "@types/express": "*", @@ -35,7 +35,7 @@ "cross-env": "^10.1.0", "dotenv-cli": "^11.0.0", "nodemon": "*", - "prisma": "^7.1.0", + "prisma": "^7.2.0", "ts-node": "*", "typescript": "*" } diff --git a/server/prisma.config.ts b/server/prisma.config.ts index 4f1306e..8a636c6 100644 --- a/server/prisma.config.ts +++ b/server/prisma.config.ts @@ -1,4 +1,6 @@ -import 'dotenv/config'; +import dotenv from 'dotenv'; +import path from 'path'; +dotenv.config({ path: path.resolve(process.cwd(), '../.env') }); import { defineConfig, env } from 'prisma/config'; export default defineConfig({ diff --git a/server/prod.db b/server/prod.db index a45f0c2a93e432408e88ef57def909e4cc8b78a6..5f7691d26b1f2c05a9b234e58547ba3a79ca276c 100644 GIT binary patch delta 1135 zcmcJOOH30%7=ZV0r_=4WbX%a57_jBp23VMGw}k}_K7fiSL6L{YZeK{Sf=ZNw0a8rl zU?kb-#*6Wh81-O;J@f!44kVfwJg5hQ;b2U>7!nhLNHlIS93;W?;$bF}dHj#e_f1UT zi3vO-Ro&b)P!t91mNrv)?3UHdys`As8_Zp!lU_2|@dx;ZR$w0HRlBVYbOD<@t{0H~ zdLlnFJRE7ELZ0FaXOLXq;e685S6*5`mi6dO|Q?t|?3WDuAh&!oX= zqC=sENFXdk{Si*`M`N523yNGgE=Zg|A`l|Q#CW~v3nvaG#?HrwE?lV%pDX93avv!l zkh+9`*g6zH+~1WHJ1_ICQbej9AY^3l;&5+IYw}pjKzFpg@6zZ<=jpyviQyw19pS?* zC)+y(p6~(A$8)}hGd|K75E}`p6@y|mA=+w>L-mL26I+%=t#bsqRYYFYooL_#_0hN_ zaScIH>x`FtTv!zRoEYVUM2N?t5rNO9`?gr}>YV!{Tb4z?GInT1z8hz3o!eg^8Ihb(*674DY)oG%7WG@cG-$^ zUNWo9GZWCS@E|>n^9)01U9;yM(v3Ql%}U^)P?`0BLpk690rHg*4>W@_vlW9p#p#7& zDEM>XxED(GQ$=>=iC5!0KmFPZ8=$Mt%>W)WJ2YkKjJG>drYc$GR~3|kEv>mX1v^Ua ymUULAYuZtAx2-n-*Zn0Xm>&}Noi4y=jbhhSJNe9Fo8DszC(`T9@Wqu4nZE(4y(@hH delta 1171 zcmcJOOH30{6o%)}ncL}f=zvh$5VQo#OPMm$nLY;Pq4JObvOu8Xf^?W^!>b@@1Pu{V zOk5(fm_(3;j~F+CjInWHh|#?vIbYpUJlXGtFIsdu$|E8wU z)D)VPk6#}lFbo4`$K0_TX2;>CUecGyb?P=+BNr^s(IR}XOu{_OKfA*K3S}WzbCLEM z-1_5E$iuUf#hE%@28G~Am6SmeG6D|m6^J-Ey4w4ca@+mq#1Ezg<{q!F^t0+9Oq3h?2!q=(S9$wENJ5!)mH zGMnUUDawsolNO7%MwJ7;V($V1N3M2uSD$u;E;J&Jr-0XBOpTG96<}^oR|C}%nkHN} zvavB75_u`82y9sCRalV^Dr{I5WR_Q@px7IZN+BT{RfhT!?8xY_QX8KbiVY55^F{l- ze%Z@&-dO)cSKY8LuJW<2_E>#KXMJ}|v#-B75wDP1TEm0=(u5RgZ|hP51Jc=n)}}Mv zry`@5PuI0Jo%eJ80L%N$b%6_4ib5qX_{4y4g5xSV?tnvM+w+AvmWJjP4lgk9k!7Yu zhzgt}$RRc;n`4TSDzI`ez_Y5%Nl_&xhW$z)omXg36XPod9~o~t1$o2KBEs6gBP+=bH%W=$LfBFXW$R2e=)q2flltD)o!Ou&A@D6%!ZN8ePSGL UOUSkdO=`0TzIxIz?R~D`7Z { + app.get('*all', (req, res) => { if (!req.path.startsWith('/api')) { res.sendFile(path.join(distPath, 'index.html')); } else { diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 6dcdb43..a934cf7 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -68,20 +68,30 @@ export class AuthService { }); // Seed default exercises + // Seed default exercises + await this.seedDefaultExercises(user.id); + + const token = jwt.sign({ userId: user.id, role: user.role }, JWT_SECRET); + const { password: _, ...userSafe } = user; + + return { user: userSafe, token }; + } + + static async seedDefaultExercises(userId: string) { try { - // Ensure env is loaded (in case server didn't restart) + // Ensure env is loaded from root (in case server didn't restart) if (!process.env.DEFAULT_EXERCISES_CSV_PATH) { - dotenv.config({ path: path.resolve(process.cwd(), '.env'), override: true }); - if (!process.env.DEFAULT_EXERCISES_CSV_PATH) { - // Try root if CWD is server - dotenv.config({ path: path.resolve(process.cwd(), '../.env'), override: true }); + const rootEnv = path.resolve(process.cwd(), '../.env'); + if (fs.existsSync(rootEnv)) { + dotenv.config({ path: rootEnv, override: true }); + } else { + dotenv.config({ path: path.resolve(process.cwd(), '.env'), override: true }); } } const csvPath = process.env.DEFAULT_EXERCISES_CSV_PATH; if (csvPath) { - // ... logic continues let resolvedPath = path.resolve(process.cwd(), csvPath); // Try to handle if resolvedPath doesn't exist but relative to root does (if CWD is server) @@ -109,7 +119,7 @@ export class AuthService { if (row.name && row.type) { exercisesToCreate.push({ - userId: user.id, + userId, name: row.name, type: row.type, bodyWeightPercentage: row.bodyWeightPercentage ? parseFloat(row.bodyWeightPercentage) : 0, @@ -120,9 +130,16 @@ export class AuthService { } if (exercisesToCreate.length > 0) { - await prisma.exercise.createMany({ - data: exercisesToCreate - }); + // Check if exercises already exist for this user to avoid duplicates + const existingCount = await prisma.exercise.count({ where: { userId } }); + if (existingCount === 0) { + await prisma.exercise.createMany({ + data: exercisesToCreate + }); + console.log(`[AuthService] Seeded ${exercisesToCreate.length} exercises for user: ${userId}`); + } else { + console.log(`[AuthService] User ${userId} already has ${existingCount} exercises. Skipping seed.`); + } } } } else { @@ -134,11 +151,6 @@ export class AuthService { console.error('[AuthService] Failed to seed default exercises:', error); // Non-blocking error } - - const token = jwt.sign({ userId: user.id, role: user.role }, JWT_SECRET); - const { password: _, ...userSafe } = user; - - return { user: userSafe, token }; } static async changePassword(userId: string, newPassword: string) {