Backend is here. Default admin is created if needed.
This commit is contained in:
6
server/.env
Normal file
6
server/.env
Normal file
@@ -0,0 +1,6 @@
|
||||
PORT=3002
|
||||
DATABASE_URL="file:./dev.db"
|
||||
JWT_SECRET="supersecretkey_change_in_production"
|
||||
API_KEY="AIzaSyCiu9gD-BcsbyIT1qpPIJrKvz_2sVyZE9A"
|
||||
ADMIN_EMAIL=admin@gymflow.ai
|
||||
ADMIN_PASSWORD=admin123
|
||||
12
server/admin_check.js
Normal file
12
server/admin_check.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
(async () => {
|
||||
const prisma = new PrismaClient();
|
||||
try {
|
||||
const admin = await prisma.user.findFirst({ where: { role: 'ADMIN' } });
|
||||
console.log('Admin user:', admin);
|
||||
} catch (e) {
|
||||
console.error('Error:', e);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
})();
|
||||
2136
server/package-lock.json
generated
Normal file
2136
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
server/package.json
Normal file
31
server/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "gymflow-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend for GymFlow AI",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"start": "node dist/index.js",
|
||||
"dev": "nodemon src/index.ts",
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"@prisma/client": "*",
|
||||
"bcryptjs": "*",
|
||||
"cors": "*",
|
||||
"dotenv": "*",
|
||||
"express": "*",
|
||||
"jsonwebtoken": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "*",
|
||||
"@types/cors": "*",
|
||||
"@types/express": "*",
|
||||
"@types/jsonwebtoken": "*",
|
||||
"@types/node": "*",
|
||||
"nodemon": "*",
|
||||
"prisma": "*",
|
||||
"ts-node": "*",
|
||||
"typescript": "*"
|
||||
}
|
||||
}
|
||||
BIN
server/prisma/dev.db
Normal file
BIN
server/prisma/dev.db
Normal file
Binary file not shown.
84
server/prisma/schema.prisma
Normal file
84
server/prisma/schema.prisma
Normal file
@@ -0,0 +1,84 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
password String
|
||||
role String @default("USER") // USER, ADMIN
|
||||
isFirstLogin Boolean @default(true)
|
||||
isBlocked Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
profile UserProfile?
|
||||
sessions WorkoutSession[]
|
||||
exercises Exercise[]
|
||||
plans WorkoutPlan[]
|
||||
}
|
||||
|
||||
model UserProfile {
|
||||
id String @id @default(uuid())
|
||||
userId String @unique
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
weight Float?
|
||||
}
|
||||
|
||||
model Exercise {
|
||||
id String @id @default(uuid())
|
||||
userId String? // Null means system default
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
type String // STRENGTH, CARDIO, BODYWEIGHT, STATIC
|
||||
bodyWeightPercentage Float? @default(0)
|
||||
isArchived Boolean @default(false)
|
||||
|
||||
sets WorkoutSet[]
|
||||
}
|
||||
|
||||
model WorkoutSession {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
startTime DateTime
|
||||
endTime DateTime?
|
||||
userBodyWeight Float?
|
||||
note String?
|
||||
|
||||
sets WorkoutSet[]
|
||||
}
|
||||
|
||||
model WorkoutSet {
|
||||
id String @id @default(uuid())
|
||||
sessionId String
|
||||
session WorkoutSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||||
exerciseId String
|
||||
exercise Exercise @relation(fields: [exerciseId], references: [id])
|
||||
|
||||
order Int
|
||||
weight Float?
|
||||
reps Int?
|
||||
distanceMeters Float?
|
||||
durationSeconds Int?
|
||||
completed Boolean @default(true)
|
||||
}
|
||||
|
||||
model WorkoutPlan {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
description String?
|
||||
exercises String // JSON string of exercise IDs
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
76
server/src/index.ts
Normal file
76
server/src/index.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import dotenv from 'dotenv';
|
||||
import authRoutes from './routes/auth';
|
||||
import exerciseRoutes from './routes/exercises';
|
||||
import sessionRoutes from './routes/sessions';
|
||||
import planRoutes from './routes/plans';
|
||||
import aiRoutes from './routes/ai';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Ensure a default admin user exists on startup
|
||||
// -------------------------------------------------------------------
|
||||
async function ensureAdminUser() {
|
||||
const adminEmail = process.env.ADMIN_EMAIL || 'admin@gymflow.ai';
|
||||
const adminPassword = process.env.ADMIN_PASSWORD || 'admin123';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Check for existing admin
|
||||
const existingAdmin = await prisma.user.findFirst({
|
||||
where: { role: 'ADMIN' },
|
||||
});
|
||||
|
||||
if (existingAdmin) {
|
||||
console.info(`✅ Admin user already exists (email: ${existingAdmin.email})`);
|
||||
await prisma.$disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create admin user
|
||||
const hashed = await bcrypt.hash(adminPassword, 10);
|
||||
const admin = await prisma.user.create({
|
||||
data: {
|
||||
email: adminEmail,
|
||||
password: hashed,
|
||||
role: 'ADMIN',
|
||||
profile: { create: { weight: 70 } },
|
||||
},
|
||||
});
|
||||
|
||||
console.info(`🛠️ Created default admin user (email: ${admin.email})`);
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Routes
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/exercises', exerciseRoutes);
|
||||
app.use('/api/sessions', sessionRoutes);
|
||||
app.use('/api/plans', planRoutes);
|
||||
app.use('/api/ai', aiRoutes);
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.send('GymFlow AI API is running');
|
||||
});
|
||||
|
||||
ensureAdminUser()
|
||||
.catch((e) => {
|
||||
console.error('❌ Failed to ensure admin user:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => {
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
});
|
||||
52
server/src/routes/ai.ts
Normal file
52
server/src/routes/ai.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import express from 'express';
|
||||
import { GoogleGenerativeAI } from '@google/generative-ai';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const router = express.Router();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
|
||||
const API_KEY = process.env.API_KEY;
|
||||
const MODEL_ID = 'gemini-1.5-flash';
|
||||
|
||||
const authenticate = (req: any, res: any, next: any) => {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
if (!token) return res.status(401).json({ error: 'Unauthorized' });
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as any;
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
};
|
||||
|
||||
router.use(authenticate);
|
||||
|
||||
router.post('/chat', async (req, res) => {
|
||||
try {
|
||||
const { history, message } = req.body;
|
||||
|
||||
if (!API_KEY) {
|
||||
return res.status(500).json({ error: 'AI service not configured' });
|
||||
}
|
||||
|
||||
const ai = new GoogleGenerativeAI(API_KEY);
|
||||
|
||||
const { systemInstruction, userMessage } = req.body;
|
||||
|
||||
const model = ai.getGenerativeModel({
|
||||
model: MODEL_ID,
|
||||
systemInstruction
|
||||
});
|
||||
|
||||
const result = await model.generateContent(userMessage);
|
||||
const response = result.response.text();
|
||||
|
||||
res.json({ response });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: String(error) });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
50
server/src/routes/auth.ts
Normal file
50
server/src/routes/auth.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import express from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
const router = express.Router();
|
||||
const prisma = new PrismaClient();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
|
||||
|
||||
|
||||
|
||||
// Login
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Admin check (hardcoded for now as per original logic, or we can seed it)
|
||||
// For now, let's stick to DB users, but maybe seed admin if needed.
|
||||
// The original code had hardcoded admin. Let's support that via a special check or just rely on DB.
|
||||
// Let's rely on DB for consistency, but if the user wants the specific admin account, they should register it.
|
||||
// However, to match original behavior, I'll add a check or just let them register 'admin@gymflow.ai'.
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
include: { profile: true }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(400).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
if (user.isBlocked) {
|
||||
return res.status(403).json({ error: 'Account is blocked' });
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(password, user.password);
|
||||
if (!isMatch) {
|
||||
return res.status(400).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
const token = jwt.sign({ userId: user.id, role: user.role }, JWT_SECRET);
|
||||
const { password: _, ...userSafe } = user;
|
||||
|
||||
res.json({ success: true, user: userSafe, token });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
82
server/src/routes/exercises.ts
Normal file
82
server/src/routes/exercises.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import express from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const router = express.Router();
|
||||
const prisma = new PrismaClient();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
|
||||
|
||||
// Middleware to check auth
|
||||
const authenticate = (req: any, res: any, next: any) => {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
if (!token) return res.status(401).json({ error: 'Unauthorized' });
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as any;
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
};
|
||||
|
||||
router.use(authenticate);
|
||||
|
||||
// Get all exercises (system default + user custom)
|
||||
router.get('/', async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const exercises = await prisma.exercise.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ userId: null }, // System default
|
||||
{ userId } // User custom
|
||||
],
|
||||
isArchived: false
|
||||
}
|
||||
});
|
||||
res.json(exercises);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Create/Update exercise
|
||||
router.post('/', async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const { id, name, type, bodyWeightPercentage } = req.body;
|
||||
|
||||
// If id exists and belongs to user, update. Else create.
|
||||
// Note: We can't update system exercises directly. If user edits a system exercise,
|
||||
// we should probably create a copy or handle it as a user override.
|
||||
// For simplicity, let's assume we are creating/updating user exercises.
|
||||
|
||||
if (id) {
|
||||
// Check if it exists and belongs to user
|
||||
const existing = await prisma.exercise.findUnique({ where: { id } });
|
||||
if (existing && existing.userId === userId) {
|
||||
const updated = await prisma.exercise.update({
|
||||
where: { id },
|
||||
data: { name, type, bodyWeightPercentage }
|
||||
});
|
||||
return res.json(updated);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new
|
||||
const newExercise = await prisma.exercise.create({
|
||||
data: {
|
||||
userId,
|
||||
name,
|
||||
type,
|
||||
bodyWeightPercentage
|
||||
}
|
||||
});
|
||||
res.json(newExercise);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
82
server/src/routes/plans.ts
Normal file
82
server/src/routes/plans.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import express from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const router = express.Router();
|
||||
const prisma = new PrismaClient();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
|
||||
|
||||
const authenticate = (req: any, res: any, next: any) => {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
if (!token) return res.status(401).json({ error: 'Unauthorized' });
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as any;
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
};
|
||||
|
||||
router.use(authenticate);
|
||||
|
||||
// Get all plans
|
||||
router.get('/', async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const plans = await prisma.workoutPlan.findMany({
|
||||
where: { userId }
|
||||
});
|
||||
res.json(plans);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Save plan
|
||||
router.post('/', async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const { id, name, description, exercises } = req.body;
|
||||
|
||||
const existing = await prisma.workoutPlan.findUnique({ where: { id } });
|
||||
|
||||
if (existing) {
|
||||
const updated = await prisma.workoutPlan.update({
|
||||
where: { id },
|
||||
data: { name, description, exercises }
|
||||
});
|
||||
return res.json(updated);
|
||||
} else {
|
||||
const created = await prisma.workoutPlan.create({
|
||||
data: {
|
||||
id,
|
||||
userId,
|
||||
name,
|
||||
description,
|
||||
exercises
|
||||
}
|
||||
});
|
||||
return res.json(created);
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete plan
|
||||
router.delete('/:id', async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const { id } = req.params;
|
||||
await prisma.workoutPlan.delete({
|
||||
where: { id, userId }
|
||||
});
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
121
server/src/routes/sessions.ts
Normal file
121
server/src/routes/sessions.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import express from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const router = express.Router();
|
||||
const prisma = new PrismaClient();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
|
||||
|
||||
const authenticate = (req: any, res: any, next: any) => {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
if (!token) return res.status(401).json({ error: 'Unauthorized' });
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as any;
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
};
|
||||
|
||||
router.use(authenticate);
|
||||
|
||||
// Get all sessions
|
||||
router.get('/', async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const sessions = await prisma.workoutSession.findMany({
|
||||
where: { userId },
|
||||
include: { sets: { include: { exercise: true } } },
|
||||
orderBy: { startTime: 'desc' }
|
||||
});
|
||||
res.json(sessions);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Save session (create or update)
|
||||
router.post('/', async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const { id, startTime, endTime, userBodyWeight, note, sets } = req.body;
|
||||
|
||||
// Check if session exists
|
||||
const existing = await prisma.workoutSession.findUnique({ where: { id } });
|
||||
|
||||
if (existing) {
|
||||
// Update
|
||||
// First delete existing sets to replace them (simplest strategy for now)
|
||||
await prisma.workoutSet.deleteMany({ where: { sessionId: id } });
|
||||
|
||||
const updated = await prisma.workoutSession.update({
|
||||
where: { id },
|
||||
data: {
|
||||
startTime,
|
||||
endTime,
|
||||
userBodyWeight,
|
||||
note,
|
||||
sets: {
|
||||
create: sets.map((s: any, idx: number) => ({
|
||||
exerciseId: s.exerciseId,
|
||||
order: idx,
|
||||
weight: s.weight,
|
||||
reps: s.reps,
|
||||
distanceMeters: s.distanceMeters,
|
||||
durationSeconds: s.durationSeconds,
|
||||
completed: s.completed
|
||||
}))
|
||||
}
|
||||
},
|
||||
include: { sets: true }
|
||||
});
|
||||
return res.json(updated);
|
||||
} else {
|
||||
// Create
|
||||
const created = await prisma.workoutSession.create({
|
||||
data: {
|
||||
id, // Use provided ID or let DB gen? Frontend usually generates UUIDs.
|
||||
userId,
|
||||
startTime,
|
||||
endTime,
|
||||
userBodyWeight,
|
||||
note,
|
||||
sets: {
|
||||
create: sets.map((s: any, idx: number) => ({
|
||||
exerciseId: s.exerciseId,
|
||||
order: idx,
|
||||
weight: s.weight,
|
||||
reps: s.reps,
|
||||
distanceMeters: s.distanceMeters,
|
||||
durationSeconds: s.durationSeconds,
|
||||
completed: s.completed
|
||||
}))
|
||||
}
|
||||
},
|
||||
include: { sets: true }
|
||||
});
|
||||
return res.json(created);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete session
|
||||
router.delete('/:id', async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const { id } = req.params;
|
||||
await prisma.workoutSession.delete({
|
||||
where: { id, userId } // Ensure user owns it
|
||||
});
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
1
server/src/test.ts
Normal file
1
server/src/test.ts
Normal file
@@ -0,0 +1 @@
|
||||
console.log("This is a test file");
|
||||
18
server/tsconfig.json
Normal file
18
server/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
1
server/tsconfig.tsbuildinfo
Normal file
1
server/tsconfig.tsbuildinfo
Normal file
@@ -0,0 +1 @@
|
||||
{"root":["./src/index.ts","./src/routes/ai.ts","./src/routes/auth.ts","./src/routes/exercises.ts","./src/routes/plans.ts","./src/routes/sessions.ts"],"version":"5.9.3"}
|
||||
Reference in New Issue
Block a user