Backend is here. Default admin is created if needed.

This commit is contained in:
aodulov
2025-11-19 10:48:37 +02:00
parent 10819cc6f5
commit bb705c8a63
25 changed files with 3662 additions and 944 deletions

6
server/.env Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

31
server/package.json Normal file
View 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

Binary file not shown.

View 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
View 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
View 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
View 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;

View 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;

View 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;

View 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
View File

@@ -0,0 +1 @@
console.log("This is a test file");

18
server/tsconfig.json Normal file
View 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"
]
}

View 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"}