NAS deployment fixed
This commit is contained in:
4
.env
4
.env
@@ -12,8 +12,8 @@ ADMIN_PASSWORD_TEST="admin123"
|
|||||||
|
|
||||||
# PROD
|
# PROD
|
||||||
DATABASE_URL_PROD="file:./prisma/prod.db"
|
DATABASE_URL_PROD="file:./prisma/prod.db"
|
||||||
ADMIN_EMAIL_PROD="admin-prod@gymflow.ai"
|
ADMIN_EMAIL_PROD="ag@gymflow.ai"
|
||||||
ADMIN_PASSWORD_PROD="secure-prod-password-change-me"
|
ADMIN_PASSWORD_PROD="masterbladdercontrol12"
|
||||||
|
|
||||||
# Fallback for Prisma CLI (Migrate default)
|
# Fallback for Prisma CLI (Migrate default)
|
||||||
DATABASE_URL="file:./prisma/dev.db"
|
DATABASE_URL="file:./prisma/dev.db"
|
||||||
|
|||||||
12
Dockerfile.node-apps
Normal file
12
Dockerfile.node-apps
Normal file
@@ -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
|
||||||
@@ -1,13 +1,48 @@
|
|||||||
// Simple script to check for admin user
|
|
||||||
const { PrismaClient } = require('@prisma/client');
|
const { PrismaClient } = require('@prisma/client');
|
||||||
(async () => {
|
const path = require('path');
|
||||||
const prisma = new PrismaClient();
|
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 {
|
try {
|
||||||
const admin = await prisma.user.findFirst({ where: { role: 'ADMIN' } });
|
const admin = await prisma.user.findFirst({
|
||||||
console.log('Admin user:', admin);
|
where: { role: 'ADMIN' },
|
||||||
} catch (e) {
|
});
|
||||||
console.error('Error:', e);
|
|
||||||
|
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 {
|
} finally {
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
}
|
}
|
||||||
})();
|
}
|
||||||
|
|
||||||
|
checkAdmin();
|
||||||
|
|||||||
@@ -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:
|
Run the following in the project root:
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
|
npm install
|
||||||
npm run build
|
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.
|
This creates a `dist` folder with the compiled frontend.
|
||||||
|
|
||||||
### Build Backend
|
### 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
|
```bash
|
||||||
cd server
|
cd server
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
cd ..
|
cd ..
|
||||||
```
|
```
|
||||||
This creates a `server/dist` folder with the compiled backend.
|
|
||||||
|
|
||||||
## 2. Transfer Files
|
## 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.*
|
*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`
|
### 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
|
```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.*
|
*Note: We assume `gymflow` is a sibling of `ag-beats` etc.*
|
||||||
|
|
||||||
**Full Command Example:**
|
**Full Command Example:**
|
||||||
```yaml
|
```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`
|
### Update `ports`
|
||||||
Add the GymFlow port (3003 inside container, mapped to your choice, e.g., 3033):
|
Add the GymFlow port (3003 inside container, mapped to your choice, e.g., 3033):
|
||||||
```yaml
|
```yaml
|
||||||
@@ -64,12 +95,45 @@ ports:
|
|||||||
- "3033:3003" # GymFlow
|
- "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`.
|
```javascript
|
||||||
- Port: `3003` (Internal)
|
module.exports = {
|
||||||
- Database: `server/prod.db` (SQLite)
|
apps: [
|
||||||
- Node Env: `production`
|
{
|
||||||
|
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
|
## 5. Nginx Proxy Manager
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ module.exports = {
|
|||||||
max_memory_restart: '1G',
|
max_memory_restart: '1G',
|
||||||
env: {
|
env: {
|
||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
|
APP_MODE: 'prod',
|
||||||
PORT: 3003,
|
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.
|
// Actually, prisma runs from where schema is or based on env.
|
||||||
// Let's assume the DB is in the root or server dir.
|
// 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"
|
// In package.json: "start:prod": "cross-env APP_MODE=prod DATABASE_URL=file:./prod.db ... src/index.ts"
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -4508,12 +4508,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jws": {
|
"node_modules/jws": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||||
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jwa": "^2.0.0",
|
"jwa": "^2.0.1",
|
||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
22
server/.env
22
server/.env
@@ -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"
|
|
||||||
@@ -4,17 +4,17 @@
|
|||||||
"description": "Backend for GymFlow AI",
|
"description": "Backend for GymFlow AI",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npm run start:prod",
|
"start": "node dist/index.js",
|
||||||
"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:prod": "node dist/index.js",
|
||||||
"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",
|
"start:dev": "cross-env APP_MODE=dev 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",
|
"dev": "npm run start:dev",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"migrate:deploy": "npx prisma migrate deploy"
|
"migrate:deploy": "npx prisma migrate deploy"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/generative-ai": "^0.24.1",
|
"@google/generative-ai": "^0.24.1",
|
||||||
"@prisma/adapter-better-sqlite3": "^7.1.0",
|
"@prisma/adapter-better-sqlite3": "^7.2.0",
|
||||||
"@prisma/client": "^7.1.0",
|
"@prisma/client": "^7.2.0",
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"bcryptjs": "3.0.3",
|
"bcryptjs": "3.0.3",
|
||||||
"better-sqlite3": "^11.0.0",
|
"better-sqlite3": "^11.0.0",
|
||||||
@@ -22,11 +22,11 @@
|
|||||||
"dotenv": "17.2.3",
|
"dotenv": "17.2.3",
|
||||||
"express": "5.1.0",
|
"express": "5.1.0",
|
||||||
"jsonwebtoken": "9.0.2",
|
"jsonwebtoken": "9.0.2",
|
||||||
"ts-node-dev": "^2.0.0",
|
|
||||||
"winston": "^3.19.0",
|
"winston": "^3.19.0",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"ts-node-dev": "^2.0.0",
|
||||||
"@types/bcryptjs": "*",
|
"@types/bcryptjs": "*",
|
||||||
"@types/cors": "*",
|
"@types/cors": "*",
|
||||||
"@types/express": "*",
|
"@types/express": "*",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"nodemon": "*",
|
"nodemon": "*",
|
||||||
"prisma": "^7.1.0",
|
"prisma": "^7.2.0",
|
||||||
"ts-node": "*",
|
"ts-node": "*",
|
||||||
"typescript": "*"
|
"typescript": "*"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
import { defineConfig, env } from 'prisma/config';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|||||||
BIN
server/prod.db
BIN
server/prod.db
Binary file not shown.
80
server/reset_prod_db.js
Normal file
80
server/reset_prod_db.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
const { execSync } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const { PrismaClient } = require('@prisma/client');
|
||||||
|
const dotenv = require('dotenv');
|
||||||
|
|
||||||
|
// Load .env from root
|
||||||
|
const envPath = path.resolve(process.cwd(), '../.env');
|
||||||
|
if (fs.existsSync(envPath)) {
|
||||||
|
dotenv.config({ path: envPath });
|
||||||
|
} else {
|
||||||
|
// Try current dir just in case, but preference is root
|
||||||
|
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetDb() {
|
||||||
|
const adminEmail = process.env.ADMIN_EMAIL_PROD || 'admin-prod@gymflow.ai';
|
||||||
|
const adminPassword = process.env.ADMIN_PASSWORD_PROD || 'secure-prod-password-change-me';
|
||||||
|
|
||||||
|
// 1. Determine DB path (relative to server dir where app runs)
|
||||||
|
const dbPath = path.resolve(process.cwd(), 'prod.db');
|
||||||
|
const prismaSchemaPath = path.resolve(process.cwd(), 'prisma/schema.prisma');
|
||||||
|
|
||||||
|
console.log(`--- Database Reset ---`);
|
||||||
|
console.log(`Admin Email: ${adminEmail}`);
|
||||||
|
console.log(`DB Path: ${dbPath}`);
|
||||||
|
|
||||||
|
// 2. Delete existing DB
|
||||||
|
if (fs.existsSync(dbPath)) {
|
||||||
|
console.log(`Deleting existing database...`);
|
||||||
|
fs.unlinkSync(dbPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Initialize fresh DB schema using Prisma
|
||||||
|
console.log(`Initializing schema via Prisma...`);
|
||||||
|
try {
|
||||||
|
// Set DATABASE_URL for prisma CLI (used by prisma.config.ts)
|
||||||
|
const absoluteDbPath = `file:${dbPath}`;
|
||||||
|
console.log(`Setting DATABASE_URL=${absoluteDbPath}`);
|
||||||
|
|
||||||
|
execSync(`npx prisma db push`, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
DATABASE_URL: absoluteDbPath
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to initialize schema:`, error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Create the Admin user
|
||||||
|
console.log(`Creating fresh admin user...`);
|
||||||
|
|
||||||
|
// In Prisma 7, we must use the adapter for better-sqlite3
|
||||||
|
const { PrismaBetterSqlite3 } = require('@prisma/adapter-better-sqlite3');
|
||||||
|
const adapter = new PrismaBetterSqlite3({ url: dbPath });
|
||||||
|
const prisma = new PrismaClient({ adapter });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const hashedPassword = await bcrypt.hash(adminPassword, 10);
|
||||||
|
await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: adminEmail,
|
||||||
|
password: hashedPassword,
|
||||||
|
role: 'ADMIN',
|
||||||
|
profile: { create: { weight: 70 } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(`✅ Success! Admin user created.`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to create admin:`, error.message);
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetDb();
|
||||||
@@ -21,9 +21,15 @@ export class AuthController {
|
|||||||
static async login(req: any, res: Response) {
|
static async login(req: any, res: Response) {
|
||||||
try {
|
try {
|
||||||
const { email, password } = req.body;
|
const { email, password } = req.body;
|
||||||
|
console.log(`[AuthController] Attempting login for: ${email}`);
|
||||||
|
|
||||||
const result = await AuthService.login(email, password);
|
const result = await AuthService.login(email, password);
|
||||||
|
console.log(`[AuthController] Login successful for: ${email}`);
|
||||||
|
|
||||||
return sendSuccess(res, result);
|
return sendSuccess(res, result);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
console.error(`[AuthController] Login failed for: ${req.body?.email}. Error: ${error.message}`);
|
||||||
|
|
||||||
if (error.message === 'Invalid credentials') {
|
if (error.message === 'Invalid credentials') {
|
||||||
return sendError(res, error.message, 400);
|
return sendError(res, error.message, 400);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,8 +56,16 @@ async function ensureAdminUser() {
|
|||||||
where: { role: 'ADMIN' },
|
where: { role: 'ADMIN' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { AuthService } = await import('./services/auth.service');
|
||||||
|
|
||||||
if (existingAdmin) {
|
if (existingAdmin) {
|
||||||
console.info(`✅ Admin user already exists (email: ${existingAdmin.email})`);
|
if (existingAdmin.email === adminEmail) {
|
||||||
|
console.info(`✅ Admin user already exists (email: ${existingAdmin.email})`);
|
||||||
|
} else {
|
||||||
|
console.info(`ℹ️ Admin user exists but with different email: ${existingAdmin.email}. Expected: ${adminEmail}`);
|
||||||
|
}
|
||||||
|
// Even if admin exists, ensure exercises are seeded (will skip if already has them)
|
||||||
|
await AuthService.seedDefaultExercises(existingAdmin.id);
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -73,7 +81,10 @@ async function ensureAdminUser() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.info(`🛠️ Created default admin user (email: ${admin.email})`);
|
// Seed exercises for new admin
|
||||||
|
await AuthService.seedDefaultExercises(admin.id);
|
||||||
|
|
||||||
|
console.info(`✅ Admin user created and exercises seeded (email: ${adminEmail})`);
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +108,7 @@ if (process.env.NODE_ENV === 'production') {
|
|||||||
const distPath = path.join(__dirname, '../../dist');
|
const distPath = path.join(__dirname, '../../dist');
|
||||||
app.use(express.static(distPath));
|
app.use(express.static(distPath));
|
||||||
|
|
||||||
app.get('*', (req, res) => {
|
app.get('*all', (req, res) => {
|
||||||
if (!req.path.startsWith('/api')) {
|
if (!req.path.startsWith('/api')) {
|
||||||
res.sendFile(path.join(distPath, 'index.html'));
|
res.sendFile(path.join(distPath, 'index.html'));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -68,20 +68,30 @@ export class AuthService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Seed default exercises
|
// 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 {
|
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) {
|
if (!process.env.DEFAULT_EXERCISES_CSV_PATH) {
|
||||||
dotenv.config({ path: path.resolve(process.cwd(), '.env'), override: true });
|
const rootEnv = path.resolve(process.cwd(), '../.env');
|
||||||
if (!process.env.DEFAULT_EXERCISES_CSV_PATH) {
|
if (fs.existsSync(rootEnv)) {
|
||||||
// Try root if CWD is server
|
dotenv.config({ path: rootEnv, override: true });
|
||||||
dotenv.config({ path: path.resolve(process.cwd(), '../.env'), override: true });
|
} else {
|
||||||
|
dotenv.config({ path: path.resolve(process.cwd(), '.env'), override: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const csvPath = process.env.DEFAULT_EXERCISES_CSV_PATH;
|
const csvPath = process.env.DEFAULT_EXERCISES_CSV_PATH;
|
||||||
|
|
||||||
if (csvPath) {
|
if (csvPath) {
|
||||||
// ... logic continues
|
|
||||||
let resolvedPath = path.resolve(process.cwd(), csvPath);
|
let resolvedPath = path.resolve(process.cwd(), csvPath);
|
||||||
|
|
||||||
// Try to handle if resolvedPath doesn't exist but relative to root does (if CWD is server)
|
// 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) {
|
if (row.name && row.type) {
|
||||||
exercisesToCreate.push({
|
exercisesToCreate.push({
|
||||||
userId: user.id,
|
userId,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
type: row.type,
|
type: row.type,
|
||||||
bodyWeightPercentage: row.bodyWeightPercentage ? parseFloat(row.bodyWeightPercentage) : 0,
|
bodyWeightPercentage: row.bodyWeightPercentage ? parseFloat(row.bodyWeightPercentage) : 0,
|
||||||
@@ -120,9 +130,16 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (exercisesToCreate.length > 0) {
|
if (exercisesToCreate.length > 0) {
|
||||||
await prisma.exercise.createMany({
|
// Check if exercises already exist for this user to avoid duplicates
|
||||||
data: exercisesToCreate
|
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 {
|
} else {
|
||||||
@@ -134,11 +151,6 @@ export class AuthService {
|
|||||||
console.error('[AuthService] Failed to seed default exercises:', error);
|
console.error('[AuthService] Failed to seed default exercises:', error);
|
||||||
// Non-blocking 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) {
|
static async changePassword(userId: string, newPassword: string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user