Default Exercises Set for new User
This commit is contained in:
22
.env
Normal file
22
.env
Normal file
@@ -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'
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -9,13 +9,13 @@ lerna-debug.log*
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
.env
|
#.env
|
||||||
*.local
|
*.local
|
||||||
server/prisma/dev.db
|
server/prisma/dev.db
|
||||||
test-results/
|
test-results/
|
||||||
playwright-report/
|
playwright-report/
|
||||||
|
|
||||||
.vscode/*
|
#.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
13
.vscode/mcp.json
vendored
Normal file
13
.vscode/mcp.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"servers": {
|
||||||
|
"playwright-test": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"playwright",
|
||||||
|
"run-test-mcp-server"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inputs": []
|
||||||
|
}
|
||||||
43
server/default_exercises.csv
Normal file
43
server/default_exercises.csv
Normal file
@@ -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
|
||||||
|
Binary file not shown.
@@ -1,6 +1,9 @@
|
|||||||
import prisma from '../lib/prisma';
|
import prisma from '../lib/prisma';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
|
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
|
||||||
|
|
||||||
@@ -64,6 +67,74 @@ export class AuthService {
|
|||||||
include: { profile: true }
|
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 token = jwt.sign({ userId: user.id, role: user.role }, JWT_SECRET);
|
||||||
const { password: _, ...userSafe } = user;
|
const { password: _, ...userSafe } = user;
|
||||||
|
|
||||||
|
|||||||
45
tests/default-exercises.spec.ts
Normal file
45
tests/default-exercises.spec.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user