diff --git a/.env b/.env new file mode 100644 index 0000000..8cc8b49 --- /dev/null +++ b/.env @@ -0,0 +1,22 @@ +# 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/.gitignore b/.gitignore index 550b00c..edf5547 100644 --- a/.gitignore +++ b/.gitignore @@ -9,13 +9,13 @@ lerna-debug.log* node_modules dist dist-ssr -.env +#.env *.local server/prisma/dev.db test-results/ playwright-report/ -.vscode/* +#.vscode/* !.vscode/extensions.json .idea .DS_Store diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000..f0c78a8 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,13 @@ +{ + "servers": { + "playwright-test": { + "type": "stdio", + "command": "npx", + "args": [ + "playwright", + "run-test-mcp-server" + ] + } + }, + "inputs": [] +} \ No newline at end of file diff --git a/server/default_exercises.csv b/server/default_exercises.csv new file mode 100644 index 0000000..0f25f53 --- /dev/null +++ b/server/default_exercises.csv @@ -0,0 +1,43 @@ +name,type,bodyWeightPercentage,isUnilateral +Air Squats,BODYWEIGHT,1.0,false +Barbell Row,STRENGTH,0,false +Bench Press,STRENGTH,0,false +Bicep Curl,STRENGTH,0,true +Bulgarian Split-Squat Jumps,BODYWEIGHT,1.0,true +Bulgarian Split-Squats,BODYWEIGHT,1.0,true +Burpees,BODYWEIGHT,1.0,false +Calf Raise,STRENGTH,0,true +Chin-Ups,BODYWEIGHT,1.0,false +Cycling,CARDIO,0,false +Deadlift,STRENGTH,0,false +Dips,BODYWEIGHT,1.0,false +Dumbbell Curl,STRENGTH,0,true +Dumbbell Shoulder Press,STRENGTH,0,true +Face Pull,STRENGTH,0,false +Front Squat,STRENGTH,0,false +Hammer Curl,STRENGTH,0,true +Handstand,BODYWEIGHT,1.0,false +Hip Thrust,STRENGTH,0,false +Jump Rope,CARDIO,0,false +Lat Pulldown,STRENGTH,0,false +Leg Extension,STRENGTH,0,true +Leg Press,STRENGTH,0,false +Lunges,BODYWEIGHT,1.0,true +Mountain Climbers,CARDIO,0,false +Muscle-Up,BODYWEIGHT,1.0,false +Overhead Press,STRENGTH,0,false +Plank,STATIC,0,false +Pull-Ups,BODYWEIGHT,1.0,false +Push-Ups,BODYWEIGHT,0.65,false +Romanian Deadlift,STRENGTH,0,false +Rowing,CARDIO,0,false +Running,CARDIO,0,false +Russian Twist,BODYWEIGHT,0,false +Seated Cable Row,STRENGTH,0,false +Side Plank,STATIC,0,true +Sissy Squats,BODYWEIGHT,1.0,false +Sprint,CARDIO,0,false +Squat,STRENGTH,0,false +Treadmill,CARDIO,0,false +Tricep Extension,STRENGTH,0,false +Wall-Sit,STATIC,0,false diff --git a/server/prisma/dev.db b/server/prisma/dev.db index a8455d4..043723e 100644 Binary files a/server/prisma/dev.db and b/server/prisma/dev.db differ diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 35a403c..6dcdb43 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -1,6 +1,9 @@ import prisma from '../lib/prisma'; import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; +import fs from 'fs'; +import path from 'path'; +import dotenv from 'dotenv'; const JWT_SECRET = process.env.JWT_SECRET || 'secret'; @@ -64,6 +67,74 @@ export class AuthService { include: { profile: true } }); + // Seed default exercises + try { + // Ensure env is loaded (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 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) + if (!fs.existsSync(resolvedPath) && !path.isAbsolute(csvPath)) { + const altPath = path.resolve(process.cwd(), '..', csvPath); + if (fs.existsSync(altPath)) { + resolvedPath = altPath; + } + } + + if (fs.existsSync(resolvedPath)) { + const content = fs.readFileSync(resolvedPath, 'utf-8'); + const lines = content.split(/\r?\n/).filter(l => l.trim().length > 0); + + if (lines.length > 1) { + const headers = lines[0].split(',').map(h => h.trim()); + const exercisesToCreate = []; + + for (let i = 1; i < lines.length; i++) { + const cols = lines[i].split(',').map(c => c.trim()); + if (cols.length < headers.length) continue; + + const row: any = {}; + headers.forEach((h, idx) => row[h] = cols[idx]); + + if (row.name && row.type) { + exercisesToCreate.push({ + userId: user.id, + name: row.name, + type: row.type, + bodyWeightPercentage: row.bodyWeightPercentage ? parseFloat(row.bodyWeightPercentage) : 0, + isUnilateral: row.isUnilateral === 'true', + isArchived: false + }); + } + } + + if (exercisesToCreate.length > 0) { + await prisma.exercise.createMany({ + data: exercisesToCreate + }); + } + } + } else { + console.warn(`[AuthService] Default exercises CSV configured but not found at: ${resolvedPath}`); + } + } + } catch (error) { + try { fs.appendFileSync(path.join(process.cwd(), 'auth_debug_custom.log'), `[${new Date().toISOString()}] ERROR: ${error}\n`); } catch (e) { } + 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; diff --git a/tests/default-exercises.spec.ts b/tests/default-exercises.spec.ts new file mode 100644 index 0000000..98f4c2f --- /dev/null +++ b/tests/default-exercises.spec.ts @@ -0,0 +1,45 @@ +import { test, expect } from './fixtures'; +import { request as playwrightRequest } from '@playwright/test'; +import path from 'path'; +import fs from 'fs'; + +test('Default Exercises Creation', async ({ createUniqueUser }) => { + // 1. Create a user + const user = await createUniqueUser(); + + // 2. Fetch exercises for the user + // Create authenticated context + const apiContext = await playwrightRequest.newContext({ + baseURL: 'http://127.0.0.1:3001', + extraHTTPHeaders: { + 'Authorization': `Bearer ${user.token}` + } + }); + + const exercisesRes = await apiContext.get('/api/exercises'); + await expect(exercisesRes).toBeOK(); + const responseJson = await exercisesRes.json(); + console.log('DEBUG: Fetched exercises response:', JSON.stringify(responseJson, null, 2)); + const exercises = responseJson.data; + + // 3. Verify default exercises are present + // Checking a subset of influential exercises from the populated list + const expectedNames = ['Bench Press', 'Squat', 'Deadlift', 'Push-Ups', 'Pull-Ups', 'Running', 'Plank', 'Handstand', 'Sprint', 'Bulgarian Split-Squats']; + + for (const name of expectedNames) { + const found = exercises.find((e: any) => e.name === name); + expect(found, `Exercise ${name} should exist`).toBeDefined(); + } + + // 4. Verify properties + const dumbbellCurl = exercises.find((e: any) => e.name === 'Dumbbell Curl'); + expect(dumbbellCurl.isUnilateral).toBe(true); + expect(dumbbellCurl.type).toBe('STRENGTH'); + + const handstand = exercises.find((e: any) => e.name === 'Handstand'); + expect(handstand.type).toBe('BODYWEIGHT'); + expect(handstand.bodyWeightPercentage).toBe(1.0); + + const pushUps = exercises.find((e: any) => e.name === 'Push-Ups'); + expect(pushUps.bodyWeightPercentage).toBe(0.65); +});