Massive backend refactoring done

This commit is contained in:
AG
2025-12-10 14:56:20 +02:00
parent 502943f7d0
commit 95a5e37748
47 changed files with 1898 additions and 1416 deletions

View File

@@ -0,0 +1,50 @@
import { Request, Response } from 'express';
import { AIService } from '../services/ai.service';
import { sendSuccess, sendError } from '../utils/apiResponse';
import logger from '../utils/logger';
export class AIController {
static async chat(req: any, res: Response) {
try {
const { systemInstruction, userMessage, sessionId } = req.body;
if (!userMessage) {
return sendError(res, 'User message is required', 400);
}
const userId = req.user.userId;
const result = await AIService.chat(userId, systemInstruction, userMessage, sessionId);
return sendSuccess(res, result);
} catch (error: any) {
console.error('AI Chat Error:', error); // Keep console log for now as AI errors can be tricky
let errorMessage = 'Failed to generate AI response';
if (error.message?.includes('API key') || error.message === 'AI service not configured') {
errorMessage = 'AI service authentication failed';
if (error.message === 'AI service not configured') {
return sendError(res, errorMessage, 500);
}
} else if (error.message?.includes('quota')) {
errorMessage = 'AI service quota exceeded';
} else if (error.message) {
errorMessage = error.message;
}
logger.error('Error in chat', { error: errorMessage });
return sendError(res, errorMessage, 500);
}
}
static async clearChat(req: any, res: Response) {
try {
const userId = req.user.userId;
const sessionId = req.params.sessionId || 'default';
await AIService.clearChat(userId, sessionId);
return sendSuccess(res, null);
} catch (error) {
logger.error('Error in clearChat', { error });
return sendError(res, 'Failed to clear chat session', 500);
}
}
}

View File

@@ -0,0 +1,149 @@
import { Request, Response } from 'express';
import { AuthService } from '../services/auth.service';
import { sendSuccess, sendError } from '../utils/apiResponse';
import logger from '../utils/logger';
export class AuthController {
static async getCurrentUser(req: any, res: Response) {
try {
const userId = req.user.userId;
const user = await AuthService.getUser(userId);
if (!user) {
return sendError(res, 'User not found', 404);
}
return sendSuccess(res, { user });
} catch (error) {
logger.error('Error in getCurrentUser', { error });
return sendError(res, 'Server error', 500);
}
}
static async login(req: any, res: Response) {
try {
const { email, password } = req.body;
const result = await AuthService.login(email, password);
return sendSuccess(res, result);
} catch (error: any) {
if (error.message === 'Invalid credentials') {
return sendError(res, error.message, 400);
}
if (error.message === 'Account is blocked') {
return sendError(res, error.message, 403);
}
logger.error('Error in login', { error });
return sendError(res, 'Server error', 500);
}
}
static async register(req: any, res: Response) {
try {
const { email, password } = req.body;
const result = await AuthService.register(email, password);
return sendSuccess(res, result);
} catch (error: any) {
if (error.message === 'User already exists') {
return sendError(res, error.message, 400);
}
logger.error('Error in register', { error });
return sendError(res, 'Server error', 500);
}
}
static async changePassword(req: any, res: Response) {
try {
const { userId, newPassword } = req.body;
const tokenUserId = req.user.userId;
if (tokenUserId !== userId) {
return sendError(res, 'Forbidden', 403);
}
await AuthService.changePassword(userId, newPassword);
return sendSuccess(res, null);
} catch (error) {
logger.error('Error in changePassword', { error });
return sendError(res, 'Server error', 500);
}
}
static async updateProfile(req: any, res: Response) {
try {
const userId = req.user.userId;
await AuthService.updateProfile(userId, req.body);
return sendSuccess(res, null);
} catch (error) {
logger.error('Error in updateProfile', { error });
return sendError(res, 'Server error', 500);
}
}
static async getAllUsers(req: any, res: Response) {
try {
if (req.user.role !== 'ADMIN') {
return sendError(res, 'Admin access required', 403);
}
const users = await AuthService.getAllUsers();
return sendSuccess(res, { users });
} catch (error) {
logger.error('Error in getAllUsers', { error });
return sendError(res, 'Server error', 500);
}
}
static async deleteUser(req: any, res: Response) {
try {
if (req.user.role !== 'ADMIN') {
return sendError(res, 'Admin access required', 403);
}
const { id } = req.params;
const adminId = req.user.userId;
await AuthService.deleteUser(adminId, id);
return sendSuccess(res, null);
} catch (error: any) {
if (error.message === 'Cannot delete yourself') {
return sendError(res, error.message, 400);
}
logger.error('Error in deleteUser', { error });
return sendError(res, 'Server error', 500);
}
}
static async toggleBlockUser(req: any, res: Response) {
try {
if (req.user.role !== 'ADMIN') {
return sendError(res, 'Admin access required', 403);
}
const { id } = req.params;
const { block } = req.body;
const adminId = req.user.userId;
await AuthService.blockUser(adminId, id, block);
return sendSuccess(res, null);
} catch (error: any) {
if (error.message === 'Cannot block yourself') {
return sendError(res, error.message, 400);
}
logger.error('Error in toggleBlockUser', { error });
return sendError(res, 'Server error', 500);
}
}
static async resetUserPassword(req: any, res: Response) {
try {
if (req.user.role !== 'ADMIN') {
return sendError(res, 'Admin access required', 403);
}
const { id } = req.params;
const { newPassword } = req.body;
await AuthService.resetUserPassword(id, newPassword);
return sendSuccess(res, null);
} catch (error: any) {
if (error.message === 'Password too short') {
return sendError(res, error.message, 400);
}
logger.error('Error in resetUserPassword', { error });
return sendError(res, 'Server error', 500);
}
}
}

View File

@@ -0,0 +1,40 @@
import { Request, Response } from 'express';
import { ExerciseService } from '../services/exercise.service';
import { sendSuccess, sendError } from '../utils/apiResponse';
import logger from '../utils/logger';
export class ExerciseController {
static async getAllExercises(req: any, res: Response) {
try {
const userId = req.user.userId;
const exercises = await ExerciseService.getAllExercises(userId);
return sendSuccess(res, exercises);
} catch (error) {
logger.error('Error in getAllExercises', { error });
return sendError(res, 'Server error', 500);
}
}
static async getLastSet(req: any, res: Response) {
try {
const userId = req.user.userId;
const exerciseId = req.params.id;
const lastSet = await ExerciseService.getLastSet(userId, exerciseId);
return sendSuccess(res, { set: lastSet });
} catch (error) {
logger.error('Error in getLastSet', { error });
return sendError(res, 'Server error', 500);
}
}
static async saveExercise(req: any, res: Response) {
try {
const userId = req.user.userId;
const exercise = await ExerciseService.saveExercise(userId, req.body);
return sendSuccess(res, exercise);
} catch (error) {
logger.error('Error in saveExercise', { error });
return sendError(res, 'Server error', 500);
}
}
}

View File

@@ -0,0 +1,40 @@
import { Request, Response } from 'express';
import { PlanService } from '../services/plan.service';
import { sendSuccess, sendError } from '../utils/apiResponse';
import logger from '../utils/logger';
export class PlanController {
static async getPlans(req: any, res: Response) {
try {
const userId = req.user.userId;
const plans = await PlanService.getPlans(userId);
return sendSuccess(res, plans);
} catch (error) {
logger.error('Error in getPlans', { error });
return sendError(res, 'Server error', 500);
}
}
static async savePlan(req: any, res: Response) {
try {
const userId = req.user.userId;
const plan = await PlanService.savePlan(userId, req.body);
return sendSuccess(res, plan);
} catch (error) {
logger.error('Error in savePlan', { error });
return sendError(res, 'Server error', 500);
}
}
static async deletePlan(req: any, res: Response) {
try {
const userId = req.user.userId;
const { id } = req.params;
await PlanService.deletePlan(userId, id);
return sendSuccess(res, null);
} catch (error) {
logger.error('Error in deletePlan', { error });
return sendError(res, 'Server error', 500);
}
}
}

View File

@@ -0,0 +1,160 @@
import { Request, Response } from 'express';
import { SessionService } from '../services/session.service';
import { sendSuccess, sendError } from '../utils/apiResponse';
import logger from '../utils/logger';
export class SessionController {
static async getAllSessions(req: any, res: Response) {
try {
const userId = req.user.userId;
const sessions = await SessionService.getAllSessions(userId);
return sendSuccess(res, sessions);
} catch (error) {
logger.error('Error in getAllSessions', { error });
return sendError(res, 'Server error', 500);
}
}
static async saveSession(req: any, res: Response) {
try {
const userId = req.user.userId;
const session = await SessionService.saveSession(userId, req.body);
return sendSuccess(res, session);
} catch (error: any) {
if (error.message === 'An active session already exists') {
return sendError(res, error.message, 400);
}
logger.error('Error in saveSession', { error });
return sendError(res, 'Server error', 500);
}
}
static async getActiveSession(req: any, res: Response) {
try {
const userId = req.user.userId;
const session = await SessionService.getActiveSession(userId);
return sendSuccess(res, { session });
} catch (error) {
logger.error('Error in getActiveSession', { error });
return sendError(res, 'Server error', 500);
}
}
static async updateActiveSession(req: any, res: Response) {
try {
const userId = req.user.userId;
const session = await SessionService.updateActiveSession(userId, req.body);
return sendSuccess(res, { session });
} catch (error: any) {
if (error.message === 'Session not found') {
return sendError(res, error.message, 404);
}
logger.error('Error in updateActiveSession', { error });
return sendError(res, 'Server error', 500);
}
}
static async getTodayQuickLog(req: any, res: Response) {
try {
const userId = req.user.userId;
const session = await SessionService.getTodayQuickLog(userId);
return sendSuccess(res, { session });
} catch (error) {
logger.error('Error in getTodayQuickLog', { error });
return sendError(res, 'Server error', 500);
}
}
static async logSetToQuickLog(req: any, res: Response) {
try {
const userId = req.user.userId;
const set = await SessionService.logSetToQuickLog(userId, req.body);
return sendSuccess(res, { set });
} catch (error) {
logger.error('Error in logSetToQuickLog', { error });
return sendError(res, 'Server error', 500);
}
}
static async logSetToActiveSession(req: any, res: Response) {
try {
const userId = req.user.userId;
const result = await SessionService.logSetToActiveSession(userId, req.body);
return sendSuccess(res, result);
} catch (error: any) {
if (error.message === 'No active session found') {
return sendError(res, error.message, 404);
}
logger.error('Error in logSetToActiveSession', { error });
return sendError(res, 'Server error', 500);
}
}
static async updateSet(req: any, res: Response) {
try {
const userId = req.user.userId;
const { setId } = req.params;
const updatedSet = await SessionService.updateSet(userId, setId, req.body);
return sendSuccess(res, { updatedSet });
} catch (error: any) {
if (error.message === 'No active session found') {
return sendError(res, error.message, 404);
}
logger.error('Error in updateSet', { error });
return sendError(res, 'Server error', 500);
}
}
static async patchSet(req: any, res: Response) {
try {
const userId = req.user.userId;
const { setId } = req.params;
const updatedSet = await SessionService.patchSet(userId, setId, req.body);
return sendSuccess(res, { updatedSet });
} catch (error: any) {
if (error.message === 'No active session found') {
return sendError(res, error.message, 404);
}
logger.error('Error in patchSet', { error });
return sendError(res, 'Server error', 500);
}
}
static async deleteSet(req: any, res: Response) {
try {
const userId = req.user.userId;
const { setId } = req.params;
await SessionService.deleteSet(userId, setId);
return sendSuccess(res, null);
} catch (error: any) {
if (error.message === 'No active session found') {
return sendError(res, error.message, 404);
}
logger.error('Error in deleteSet', { error });
return sendError(res, 'Server error', 500);
}
}
static async deleteActiveSession(req: any, res: Response) {
try {
const userId = req.user.userId;
await SessionService.deleteActiveSession(userId);
return sendSuccess(res, null);
} catch (error) {
logger.error('Error in deleteActiveSession', { error });
return sendError(res, 'Server error', 500);
}
}
static async deleteSession(req: any, res: Response) {
try {
const userId = req.user.userId;
const { id } = req.params;
await SessionService.deleteSession(userId, id);
return sendSuccess(res, null);
} catch (error) {
logger.error('Error in deleteSession', { error });
return sendError(res, 'Server error', 500);
}
}
}

View File

@@ -0,0 +1,34 @@
import { Request, Response } from 'express';
import { WeightService } from '../services/weight.service';
import { sendSuccess, sendError } from '../utils/apiResponse';
import logger from '../utils/logger';
export class WeightController {
static async getWeightHistory(req: any, res: Response) {
try {
const userId = req.user.userId;
const weights = await WeightService.getWeightHistory(userId);
return sendSuccess(res, weights);
} catch (error) {
logger.error('Error in getWeightHistory', { error });
return sendError(res, 'Failed to fetch weight history', 500);
}
}
static async logWeight(req: any, res: Response) {
try {
const userId = req.user.userId;
const { weight, dateStr } = req.body;
if (!weight || !dateStr) {
return sendError(res, 'Weight and dateStr are required', 400);
}
const record = await WeightService.logWeight(userId, parseFloat(weight), dateStr);
return sendSuccess(res, record);
} catch (error) {
logger.error('Error in logWeight', { error });
return sendError(res, 'Failed to log weight', 500);
}
}
}

View File

@@ -1,5 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { sendError } from '../utils/apiResponse';
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
@@ -8,12 +9,12 @@ export const authenticateToken = (req: Request, res: Response, next: NextFunctio
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.sendStatus(401);
return sendError(res, 'Unauthorized', 401);
}
jwt.verify(token, JWT_SECRET, (err: any, user: any) => {
if (err) {
return res.sendStatus(403);
return sendError(res, 'Forbidden', 403);
}
(req as any).user = user;
next();

View File

@@ -1,5 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import { ZodSchema } from 'zod';
import { sendError } from '../utils/apiResponse';
export const validate = (schema: ZodSchema<any>) => async (req: Request, res: Response, next: NextFunction) => {
try {
@@ -10,6 +11,6 @@ export const validate = (schema: ZodSchema<any>) => async (req: Request, res: Re
});
return next();
} catch (error) {
return res.status(400).json(error);
return sendError(res, `Validation Error: ${JSON.stringify(error)}`, 400);
}
};

View File

@@ -1,116 +1,12 @@
import express, { Request, Response, NextFunction } from 'express';
import { GoogleGenerativeAI } from '@google/generative-ai';
import jwt from 'jsonwebtoken';
interface JwtPayload {
userId: string;
role: string;
}
interface AuthRequest extends Request {
user?: JwtPayload;
}
import express from 'express';
import { AIController } from '../controllers/ai.controller';
import { authenticateToken } from '../middleware/auth';
const router = express.Router();
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
const API_KEY = process.env.GEMINI_API_KEY || process.env.API_KEY;
const MODEL_ID = 'gemini-flash-lite-latest';
// Store chat sessions in memory (in production, use Redis or similar)
const chatSessions = new Map<string, any>();
router.use(authenticateToken);
const authenticate = (req: AuthRequest, res: Response, next: NextFunction) => {
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 JwtPayload;
req.user = decoded;
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
};
router.use(authenticate);
router.post('/chat', async (req: AuthRequest, res: Response) => {
try {
const { systemInstruction, userMessage, sessionId } = req.body;
if (!API_KEY) {
return res.status(500).json({ error: 'AI service not configured' });
}
if (!userMessage) {
return res.status(400).json({ error: 'User message is required' });
}
if (!req.user) {
return res.status(401).json({ error: 'Unauthorized' });
}
const ai = new GoogleGenerativeAI(API_KEY);
const userId = req.user.userId;
const chatKey = `${userId}-${sessionId || 'default'}`;
// Get or create chat session
let chat = chatSessions.get(chatKey);
if (!chat || systemInstruction) {
// Create new chat with system instruction
const model = ai.getGenerativeModel({
model: MODEL_ID,
systemInstruction: systemInstruction || 'You are a helpful fitness coach.'
});
chat = model.startChat({
history: []
});
chatSessions.set(chatKey, chat);
}
// Send message and get response
const result = await chat.sendMessage(userMessage);
const response = result.response.text();
res.json({ response });
} catch (error: any) {
console.error('AI Chat Error:', error);
// Provide more detailed error messages
let errorMessage = 'Failed to generate AI response';
if (error.message?.includes('API key')) {
errorMessage = 'AI service authentication failed';
} else if (error.message?.includes('quota')) {
errorMessage = 'AI service quota exceeded';
} else if (error.message) {
errorMessage = error.message;
}
res.status(500).json({ error: errorMessage });
}
});
// Clear chat session endpoint
router.delete('/chat/:sessionId', async (req: AuthRequest, res: Response) => {
try {
if (!req.user) {
return res.status(401).json({ error: 'Unauthorized' });
}
const userId = req.user.userId;
const sessionId = req.params.sessionId || 'default';
const chatKey = `${userId}-${sessionId}`;
chatSessions.delete(chatKey);
res.json({ success: true });
} catch (error) {
console.error('Clear chat error:', error);
res.status(500).json({ error: 'Failed to clear chat session' });
}
});
router.post('/chat', AIController.chat);
router.delete('/chat/:sessionId', AIController.clearChat);
export default router;

View File

@@ -1,300 +1,26 @@
import express from 'express';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import prisma from '../lib/prisma';
import { AuthController } from '../controllers/auth.controller';
import { validate } from '../middleware/validate';
import { authenticateToken } from '../middleware/auth';
import { loginSchema, registerSchema, changePasswordSchema, updateProfileSchema } from '../schemas/auth';
const router = express.Router();
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
// Get Current User
router.get('/me', async (req, res) => {
try {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Unauthorized' });
// Public routes
router.post('/login', validate(loginSchema), AuthController.login);
router.post('/register', validate(registerSchema), AuthController.register);
const decoded = jwt.verify(token, JWT_SECRET) as any;
const user = await prisma.user.findUnique({
where: { id: decoded.userId },
include: { profile: true }
});
// Protected routes
router.use(authenticateToken); // Standard middleware now
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
router.get('/me', AuthController.getCurrentUser);
router.post('/change-password', validate(changePasswordSchema), AuthController.changePassword);
router.patch('/profile', validate(updateProfileSchema), AuthController.updateProfile);
const { password: _, ...userSafe } = user;
res.json({ success: true, user: userSafe });
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
});
// Login
router.post('/login', validate(loginSchema), async (req, res) => {
try {
const { email, password } = req.body;
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) {
console.error('Login error:', error);
res.status(500).json({ error: 'Server error' });
}
});
// Register
router.post('/register', validate(registerSchema), async (req, res) => {
try {
const { email, password } = req.body;
// Check if user already exists
const existingUser = await prisma.user.findUnique({ where: { email } });
if (existingUser) {
return res.status(400).json({ error: 'User already exists' });
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = await prisma.user.create({
data: {
email,
password: hashedPassword,
role: 'USER',
profile: {
create: {
weight: 70
}
}
},
include: { profile: true }
});
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) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Change Password
router.post('/change-password', validate(changePasswordSchema), async (req, res) => {
try {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Unauthorized' });
const { userId, newPassword } = req.body;
// Verify token
const decoded = jwt.verify(token, JWT_SECRET) as any;
if (decoded.userId !== userId) {
return res.status(403).json({ error: 'Forbidden' });
}
const hashed = await bcrypt.hash(newPassword, 10);
await prisma.user.update({
where: { id: userId },
data: {
password: hashed,
isFirstLogin: false
}
});
res.json({ success: true });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Update Profile
router.patch('/profile', validate(updateProfileSchema), async (req, res) => {
try {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Unauthorized' });
// const { userId, profile } = req.body;
// Convert birthDate from timestamp to Date object if needed
if (req.body.birthDate) {
// Handle both number (timestamp) and string (ISO)
req.body.birthDate = new Date(req.body.birthDate);
}
// Verify token
const decoded = jwt.verify(token, JWT_SECRET) as any;
const userId = decoded.userId;
// Update or create profile
await prisma.userProfile.upsert({
where: { userId: userId },
update: {
...req.body
},
create: {
userId: userId,
...req.body
}
});
res.json({ success: true });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Admin: Get All Users
router.get('/users', async (req, res) => {
try {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Unauthorized' });
const decoded = jwt.verify(token, JWT_SECRET) as any;
if (decoded.role !== 'ADMIN') {
return res.status(403).json({ error: 'Admin access required' });
}
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
role: true,
isBlocked: true,
isFirstLogin: true,
profile: true
},
orderBy: {
email: 'asc'
}
});
res.json({ success: true, users });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Admin: Delete User
router.delete('/users/:id', async (req, res) => {
try {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Unauthorized' });
const decoded = jwt.verify(token, JWT_SECRET) as any;
if (decoded.role !== 'ADMIN') {
return res.status(403).json({ error: 'Admin access required' });
}
const { id } = req.params;
// Prevent deleting self
if (id === decoded.userId) {
return res.status(400).json({ error: 'Cannot delete yourself' });
}
await prisma.user.delete({ where: { id } });
res.json({ success: true });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Admin: Toggle Block User
router.patch('/users/:id/block', async (req, res) => {
try {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Unauthorized' });
const decoded = jwt.verify(token, JWT_SECRET) as any;
if (decoded.role !== 'ADMIN') {
return res.status(403).json({ error: 'Admin access required' });
}
const { id } = req.params;
const { block } = req.body;
// Prevent blocking self
if (id === decoded.userId) {
return res.status(400).json({ error: 'Cannot block yourself' });
}
await prisma.user.update({
where: { id },
data: { isBlocked: block }
});
res.json({ success: true });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Admin: Reset User Password
router.post('/users/:id/reset-password', async (req, res) => {
try {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Unauthorized' });
const decoded = jwt.verify(token, JWT_SECRET) as any;
if (decoded.role !== 'ADMIN') {
return res.status(403).json({ error: 'Admin access required' });
}
const { id } = req.params;
const { newPassword } = req.body;
if (!newPassword || newPassword.length < 4) {
return res.status(400).json({ error: 'Password too short' });
}
const hashed = await bcrypt.hash(newPassword, 10);
await prisma.user.update({
where: { id },
data: {
password: hashed,
isFirstLogin: true // Force them to change it on login
}
});
res.json({ success: true });
} catch (error) {
console.error('Reset password error:', error);
res.status(500).json({ error: 'Server error' });
}
});
// Admin routes
router.get('/users', AuthController.getAllUsers);
router.delete('/users/:id', AuthController.deleteUser);
router.patch('/users/:id/block', AuthController.toggleBlockUser);
router.post('/users/:id/reset-password', AuthController.resetUserPassword);
export default router;

View File

@@ -1,121 +1,13 @@
import express from 'express';
import jwt from 'jsonwebtoken';
import prisma from '../lib/prisma';
import { ExerciseController } from '../controllers/exercise.controller';
import { authenticateToken } from '../middleware/auth';
const router = express.Router();
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
// Middleware to check auth
// 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' });
router.use(authenticateToken);
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
]
}
});
res.json(exercises);
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
});
// Get last set for specific exercise
router.get('/:id/last-set', async (req: any, res) => {
try {
const userId = req.user.userId;
const exerciseId = req.params.id;
const lastSet = await prisma.workoutSet.findFirst({
where: {
exerciseId,
session: { userId } // Ensure optimization by filtering sessions of the user
},
include: {
session: true
},
orderBy: {
timestamp: 'desc'
}
});
res.json({ success: true, set: lastSet });
} catch (error) {
console.error(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, isArchived, isUnilateral } = req.body;
const data = {
name,
type,
bodyWeightPercentage: bodyWeightPercentage ? parseFloat(bodyWeightPercentage) : undefined,
isArchived: !!isArchived,
isUnilateral: !!isUnilateral
};
// 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: data
});
return res.json(updated);
}
}
// Create new
const newExercise = await prisma.exercise.create({
data: {
id: id || undefined, // Use provided ID if available
userId,
name: data.name,
type: data.type,
bodyWeightPercentage: data.bodyWeightPercentage,
isArchived: data.isArchived,
isUnilateral: data.isUnilateral,
}
});
res.json(newExercise);
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
});
router.get('/', ExerciseController.getAllExercises);
router.get('/:id/last-set', ExerciseController.getLastSet);
router.post('/', ExerciseController.saveExercise);
export default router;

View File

@@ -1,148 +1,13 @@
import express from 'express';
import jwt from 'jsonwebtoken';
import prisma from '../lib/prisma';
import { PlanController } from '../controllers/plan.controller';
import { authenticateToken } from '../middleware/auth';
const router = express.Router();
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' });
router.use(authenticateToken);
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 },
include: {
planExercises: {
include: { exercise: true },
orderBy: { order: 'asc' }
}
},
orderBy: { createdAt: 'desc' }
});
const mappedPlans = plans.map((p: any) => ({
...p,
steps: p.planExercises.map((pe: any) => ({
id: pe.id,
exerciseId: pe.exerciseId,
exerciseName: pe.exercise.name,
exerciseType: pe.exercise.type,
isWeighted: pe.isWeighted,
// Add default properties if needed by PlannedSet interface
}))
}));
res.json(mappedPlans);
} catch (error) {
console.error('Error fetching plans:', 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, steps } = req.body;
// Steps array contains PlannedSet items
// We need to transact: create/update plan, then replace exercises
await prisma.$transaction(async (tx) => {
// Upsert plan
let plan = await tx.workoutPlan.findUnique({ where: { id } });
if (plan) {
await tx.workoutPlan.update({
where: { id },
data: { name, description }
});
// Delete existing plan exercises
await tx.planExercise.deleteMany({ where: { planId: id } });
} else {
await tx.workoutPlan.create({
data: {
id,
userId,
name,
description
}
});
}
// Create new plan exercises
if (steps && steps.length > 0) {
await tx.planExercise.createMany({
data: steps.map((step: any, index: number) => ({
planId: id,
exerciseId: step.exerciseId,
order: index,
isWeighted: step.isWeighted || false
}))
});
}
});
// Return the updated plan structure
// Since we just saved it, we can mirror back what was sent or re-fetch.
// Re-fetching ensures DB state consistency.
const savedPlan = await prisma.workoutPlan.findUnique({
where: { id },
include: {
planExercises: {
include: { exercise: true },
orderBy: { order: 'asc' }
}
}
});
if (!savedPlan) throw new Error("Plan failed to save");
const mappedPlan = {
...savedPlan,
steps: savedPlan.planExercises.map((pe: any) => ({
id: pe.id,
exerciseId: pe.exerciseId,
exerciseName: pe.exercise.name,
exerciseType: pe.exercise.type,
isWeighted: pe.isWeighted
}))
};
res.json(mappedPlan);
} catch (error) {
console.error('Error saving plan:', 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' });
}
});
router.get('/', PlanController.getPlans);
router.post('/', PlanController.savePlan);
router.delete('/:id', PlanController.deletePlan);
export default router;

View File

@@ -1,587 +1,24 @@
import express from 'express';
import jwt from 'jsonwebtoken';
import prisma from '../lib/prisma';
import { SessionController } from '../controllers/session.controller';
import { validate } from '../middleware/validate';
import { authenticateToken } from '../middleware/auth';
import { sessionSchema, logSetSchema, updateSetSchema } from '../schemas/sessions';
const router = express.Router();
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,
OR: [
{ endTime: { not: null } },
{ type: 'QUICK_LOG' }
]
},
include: { sets: { include: { exercise: true } } },
orderBy: { startTime: 'desc' }
});
// Map exerciseName and type onto each set for frontend convenience
const mappedSessions = sessions.map(session => ({
...session,
sets: session.sets.map(set => ({
...set,
exerciseName: set.exercise.name,
type: set.exercise.type
}))
}));
res.json(mappedSessions);
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
});
// Save session (create or update)
router.post('/', validate(sessionSchema), async (req: any, res) => {
try {
const userId = req.user.userId;
const { id, startTime, endTime, userBodyWeight, note, planId, planName, sets } = req.body;
// Convert timestamps to Date objects if they are numbers
const start = new Date(startTime);
const end = endTime ? new Date(endTime) : null;
const weight = userBodyWeight ? parseFloat(userBodyWeight) : null;
// 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: start,
endTime: end,
userBodyWeight: weight,
note,
planId,
planName,
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 !== undefined ? s.completed : true
}))
}
},
include: { sets: true }
});
return res.json(updated);
} else {
// Create
// If creating a new active session (endTime is null), check if one already exists
if (!end) {
const active = await prisma.workoutSession.findFirst({
where: {
userId,
endTime: null,
type: 'STANDARD' // Only check for standard sessions, not Quick Log
}
});
if (active) {
return res.status(400).json({ error: 'An active session already exists' });
}
}
const created = await prisma.workoutSession.create({
data: {
id, // Use provided ID or let DB gen? Frontend usually generates UUIDs.
userId,
startTime: start,
endTime: end,
userBodyWeight: weight,
note,
planId,
planName,
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 !== undefined ? s.completed : true
}))
}
},
include: { sets: true }
});
// Update user profile weight if session has weight and is finished
if (weight && end) {
await prisma.userProfile.upsert({
where: { userId },
create: { userId, weight },
update: { weight }
});
}
return res.json(created);
}
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Get active session (session without endTime)
router.get('/active', async (req: any, res) => {
try {
const userId = req.user.userId;
const activeSession = await prisma.workoutSession.findFirst({
where: {
userId,
endTime: null,
type: 'STANDARD'
},
include: { sets: { include: { exercise: true }, orderBy: { order: 'asc' } } }
});
if (!activeSession) {
return res.json({ success: true, session: null });
}
res.json({ success: true, session: activeSession });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Update active session (for real-time set updates)
router.put('/active', validate(sessionSchema), async (req: any, res) => {
try {
const userId = req.user.userId;
const { id, startTime, endTime, userBodyWeight, note, planId, planName, sets } = req.body;
// Convert timestamps to Date objects if they are numbers
const start = new Date(startTime);
const end = endTime ? new Date(endTime) : null;
const weight = userBodyWeight ? parseFloat(userBodyWeight) : null;
// Check if session exists and belongs to user
const existing = await prisma.workoutSession.findFirst({
where: { id, userId }
});
if (!existing) {
return res.status(404).json({ error: 'Session not found' });
}
// Delete existing sets to replace them
await prisma.workoutSet.deleteMany({ where: { sessionId: id } });
const updated = await prisma.workoutSession.update({
where: { id },
data: {
startTime: start,
endTime: end,
userBodyWeight: weight,
note,
planId,
planName,
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 !== undefined ? s.completed : true
}))
}
},
include: { sets: { include: { exercise: true } } }
});
// Update user profile weight if session has weight and is finished
if (weight && end) {
await prisma.userProfile.upsert({
where: { userId },
create: { userId, weight },
update: { weight }
});
}
res.json({ success: true, session: updated });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Get today's quick log session
router.get('/quick-log', async (req: any, res) => {
try {
const userId = req.user.userId;
const startOfDay = new Date();
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999);
const session = await prisma.workoutSession.findFirst({
where: {
userId,
type: 'QUICK_LOG',
startTime: {
gte: startOfDay,
lte: endOfDay
}
},
include: { sets: { include: { exercise: true }, orderBy: { timestamp: 'desc' } } }
});
if (!session) {
return res.json({ success: true, session: null });
}
// Map exerciseName and type onto sets
const mappedSession = {
...session,
sets: session.sets.map(set => ({
...set,
exerciseName: set.exercise.name,
type: set.exercise.type
}))
};
res.json({ success: true, session: mappedSession });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Log a set to today's quick log session
router.post('/quick-log/set', validate(logSetSchema), async (req: any, res) => {
try {
const userId = req.user.userId;
const { exerciseId, weight, reps, distanceMeters, durationSeconds, height, bodyWeightPercentage, note, side } = req.body;
const startOfDay = new Date();
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999);
// Find or create today's quick log session
let session = await prisma.workoutSession.findFirst({
where: {
userId,
type: 'QUICK_LOG',
startTime: {
gte: startOfDay,
lte: endOfDay
}
}
});
if (!session) {
session = await prisma.workoutSession.create({
data: {
userId,
startTime: startOfDay,
type: 'QUICK_LOG',
note: 'Daily Quick Log'
}
});
}
// Create the set
const newSet = await prisma.workoutSet.create({
data: {
sessionId: session.id,
exerciseId,
order: 0,
weight: weight ? parseFloat(weight) : null,
reps: reps ? parseInt(reps) : null,
distanceMeters: distanceMeters ? parseFloat(distanceMeters) : null,
durationSeconds: durationSeconds ? parseInt(durationSeconds) : null,
side: side || null
},
include: { exercise: true }
});
const mappedSet = {
...newSet,
exerciseName: newSet.exercise.name,
type: newSet.exercise.type
};
res.json({ success: true, set: mappedSet });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Log a set to the active session
router.post('/active/log-set', validate(logSetSchema), async (req: any, res) => {
try {
const userId = req.user.userId;
const { exerciseId, reps, weight, distanceMeters, durationSeconds, side } = req.body;
// Find active session
const activeSession = await prisma.workoutSession.findFirst({
where: { userId, endTime: null, type: 'STANDARD' },
include: { sets: true }
});
if (!activeSession) {
return res.status(404).json({ error: 'No active session found' });
}
// Get the highest order value from the existing sets
const maxOrder = activeSession.sets.reduce((max, set) => Math.max(max, set.order), -1);
// Create the new set
const newSet = await prisma.workoutSet.create({
data: {
sessionId: activeSession.id,
exerciseId,
order: maxOrder + 1,
reps: reps ? parseInt(reps) : null,
weight: weight ? parseFloat(weight) : null,
distanceMeters: distanceMeters ? parseFloat(distanceMeters) : null,
durationSeconds: durationSeconds ? parseInt(durationSeconds) : null,
side: side || null,
completed: true
},
include: { exercise: true }
});
// Recalculate active step
if (activeSession.planId) {
const plan = await prisma.workoutPlan.findUnique({
where: { id: activeSession.planId }
});
if (plan) {
const planExercises: { id: string }[] = JSON.parse(plan.exercises || '[]');
const allPerformedSets = await prisma.workoutSet.findMany({
where: { sessionId: activeSession.id }
});
const performedCounts = new Map<string, number>();
for (const set of allPerformedSets) {
performedCounts.set(set.exerciseId, (performedCounts.get(set.exerciseId) || 0) + 1);
}
let activeExerciseId = null;
const plannedCounts = new Map<string, number>();
for (const planExercise of planExercises) {
const exerciseId = planExercise.id;
plannedCounts.set(exerciseId, (plannedCounts.get(exerciseId) || 0) + 1);
const performedCount = performedCounts.get(exerciseId) || 0;
if (performedCount < plannedCounts.get(exerciseId)!) {
activeExerciseId = exerciseId;
break;
}
}
const mappedNewSet = {
...newSet,
exerciseName: newSet.exercise.name,
type: newSet.exercise.type
};
return res.json({ success: true, newSet: mappedNewSet, activeExerciseId });
}
}
// If no plan or plan not found, just return the new set
const mappedNewSet = {
...newSet,
exerciseName: newSet.exercise.name,
type: newSet.exercise.type
};
res.json({ success: true, newSet: mappedNewSet, activeExerciseId: null });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Update a set in the active session
router.put('/active/set/:setId', async (req: any, res) => {
try {
const userId = req.user.userId;
const { setId } = req.params;
const { reps, weight, distanceMeters, durationSeconds } = req.body;
// Find active session (STANDARD or QUICK_LOG)
const activeSession = await prisma.workoutSession.findFirst({
where: { userId, endTime: null },
});
if (!activeSession) {
return res.status(404).json({ error: 'No active session found' });
}
const updatedSet = await prisma.workoutSet.update({
where: { id: setId },
data: {
reps: reps ? parseInt(reps) : null,
weight: weight ? parseFloat(weight) : null,
distanceMeters: distanceMeters ? parseFloat(distanceMeters) : null,
durationSeconds: durationSeconds ? parseInt(durationSeconds) : null,
},
include: { exercise: true }
});
const mappedUpdatedSet = {
...updatedSet,
exerciseName: updatedSet.exercise.name,
type: updatedSet.exercise.type
};
res.json({ success: true, updatedSet: mappedUpdatedSet });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Update a set in the active session (STANDARD or QUICK_LOG)
router.patch('/active/set/:setId', validate(updateSetSchema), async (req: any, res) => {
try {
const userId = req.user.userId;
const { setId } = req.params;
const { reps, weight, distanceMeters, durationSeconds, height, bodyWeightPercentage, side, note } = req.body;
// Find active session (STANDARD or QUICK_LOG)
const activeSession = await prisma.workoutSession.findFirst({
where: { userId, endTime: null },
});
if (!activeSession) {
return res.status(404).json({ error: 'No active session found' });
}
const updatedSet = await prisma.workoutSet.update({
where: { id: setId },
data: {
reps: reps !== undefined ? (reps ? parseInt(reps) : null) : undefined,
weight: weight !== undefined ? (weight ? parseFloat(weight) : null) : undefined,
distanceMeters: distanceMeters !== undefined ? (distanceMeters ? parseFloat(distanceMeters) : null) : undefined,
durationSeconds: durationSeconds !== undefined ? (durationSeconds ? parseInt(durationSeconds) : null) : undefined,
height: height !== undefined ? (height ? parseFloat(height) : null) : undefined,
bodyWeightPercentage: bodyWeightPercentage !== undefined ? (bodyWeightPercentage ? parseFloat(bodyWeightPercentage) : null) : undefined,
side: side !== undefined ? side : undefined,
},
include: { exercise: true }
});
const mappedUpdatedSet = {
...updatedSet,
exerciseName: updatedSet.exercise.name,
type: updatedSet.exercise.type
};
res.json({ success: true, updatedSet: mappedUpdatedSet });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Delete a set from the active session
router.delete('/active/set/:setId', async (req: any, res) => {
try {
const userId = req.user.userId;
const { setId } = req.params;
// Find active session (STANDARD or QUICK_LOG)
const activeSession = await prisma.workoutSession.findFirst({
where: { userId, endTime: null },
});
if (!activeSession) {
return res.status(404).json({ error: 'No active session found' });
}
await prisma.workoutSet.delete({
where: { id: setId }
});
res.json({ success: true });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Delete active session (quit without saving)
router.delete('/active', async (req: any, res) => {
try {
const userId = req.user.userId;
// Delete all active sessions for this user to ensure clean state
await prisma.workoutSession.deleteMany({
where: {
userId,
endTime: null,
type: 'STANDARD'
}
});
res.json({ success: true });
} 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' });
}
});
router.use(authenticateToken);
router.get('/', SessionController.getAllSessions);
router.post('/', validate(sessionSchema), SessionController.saveSession);
router.get('/active', SessionController.getActiveSession);
router.put('/active', validate(sessionSchema), SessionController.updateActiveSession);
router.get('/quick-log', SessionController.getTodayQuickLog);
router.post('/quick-log/set', validate(logSetSchema), SessionController.logSetToQuickLog);
router.post('/active/log-set', validate(logSetSchema), SessionController.logSetToActiveSession);
router.put('/active/set/:setId', SessionController.updateSet);
router.patch('/active/set/:setId', validate(updateSetSchema), SessionController.patchSet);
router.delete('/active/set/:setId', SessionController.deleteSet);
router.delete('/active', SessionController.deleteActiveSession);
router.delete('/:id', SessionController.deleteSession);
export default router;

View File

@@ -1,77 +1,12 @@
import express from 'express';
import { WeightController } from '../controllers/weight.controller';
import { authenticateToken } from '../middleware/auth';
import prisma from '../lib/prisma';
const router = express.Router();
// Get weight history
router.get('/', authenticateToken, async (req, res) => {
try {
const userId = (req as any).user.userId;
const weights = await prisma.bodyWeightRecord.findMany({
where: { userId },
orderBy: { date: 'desc' },
take: 365 // Limit to last year for now
});
res.json(weights);
} catch (error) {
console.error('Error fetching weight history:', error);
res.status(500).json({ error: 'Failed to fetch weight history' });
}
});
router.use(authenticateToken);
// Log weight
router.post('/', authenticateToken, async (req, res) => {
try {
const userId = (req as any).user.userId;
const { weight, dateStr } = req.body;
if (!weight || !dateStr) {
return res.status(400).json({ error: 'Weight and dateStr are required' });
}
// Upsert: Update if exists for this day, otherwise create
const record = await prisma.bodyWeightRecord.upsert({
where: {
userId_dateStr: {
userId,
dateStr
}
},
update: {
weight: parseFloat(weight),
date: new Date(dateStr) // Update date object just in case
},
create: {
userId,
weight: parseFloat(weight),
dateStr,
date: new Date(dateStr)
}
});
// Also update the user profile weight to the latest logged weight
// But only if the logged date is today or in the future (or very recent)
// For simplicity, let's just update the profile weight if it's the most recent record
// Or we can just update it always if the user considers this their "current" weight.
// Let's check if this is the latest record by date.
const latestRecord = await prisma.bodyWeightRecord.findFirst({
where: { userId },
orderBy: { date: 'desc' }
});
if (latestRecord && latestRecord.id === record.id) {
await prisma.userProfile.update({
where: { userId },
data: { weight: parseFloat(weight) }
});
}
res.json(record);
} catch (error) {
console.error('Error logging weight:', error);
res.status(500).json({ error: 'Failed to log weight' });
}
});
router.get('/', WeightController.getWeightHistory);
router.post('/', WeightController.logWeight);
export default router;

View File

@@ -0,0 +1,45 @@
import { GoogleGenerativeAI } from '@google/generative-ai';
const API_KEY = process.env.GEMINI_API_KEY || process.env.API_KEY;
const MODEL_ID = 'gemini-flash-lite-latest';
// Store chat sessions in memory
const chatSessions = new Map<string, any>();
export class AIService {
static async chat(userId: string, systemInstruction: string, userMessage: string, sessionId: string) {
if (!API_KEY) {
throw new Error('AI service not configured');
}
const chatKey = `${userId}-${sessionId || 'default'}`;
// Get or create chat session
let chat = chatSessions.get(chatKey);
if (!chat || systemInstruction) {
const ai = new GoogleGenerativeAI(API_KEY);
// Create new chat with system instruction
const model = ai.getGenerativeModel({
model: MODEL_ID,
systemInstruction: systemInstruction || 'You are a helpful fitness coach.'
});
chat = model.startChat({
history: []
});
chatSessions.set(chatKey, chat);
}
const result = await chat.sendMessage(userMessage);
const response = result.response.text();
return { response };
}
static async clearChat(userId: string, sessionId: string) {
const chatKey = `${userId}-${sessionId}`;
chatSessions.delete(chatKey);
}
}

View File

@@ -0,0 +1,147 @@
import prisma from '../lib/prisma';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
export class AuthService {
static async getUser(userId: string) {
const user = await prisma.user.findUnique({
where: { id: userId },
include: { profile: true }
});
if (!user) return null;
const { password: _, ...userSafe } = user;
return userSafe;
}
static async login(email: string, password: string) {
const user = await prisma.user.findUnique({
where: { email },
include: { profile: true }
});
if (!user) {
throw new Error('Invalid credentials');
}
if (user.isBlocked) {
throw new Error('Account is blocked');
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
throw new Error('Invalid credentials');
}
const token = jwt.sign({ userId: user.id, role: user.role }, JWT_SECRET);
const { password: _, ...userSafe } = user;
return { user: userSafe, token };
}
static async register(email: string, password: string) {
const existingUser = await prisma.user.findUnique({ where: { email } });
if (existingUser) {
throw new Error('User already exists');
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = await prisma.user.create({
data: {
email,
password: hashedPassword,
role: 'USER',
profile: {
create: {
weight: 70
}
}
},
include: { profile: true }
});
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) {
const hashed = await bcrypt.hash(newPassword, 10);
await prisma.user.update({
where: { id: userId },
data: {
password: hashed,
isFirstLogin: false
}
});
}
static async updateProfile(userId: string, data: any) {
// Convert birthDate if needed
if (data.birthDate) {
data.birthDate = new Date(data.birthDate);
}
await prisma.userProfile.upsert({
where: { userId: userId },
update: { ...data },
create: { userId: userId, ...data }
});
}
static async getAllUsers() {
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
role: true,
isBlocked: true,
isFirstLogin: true,
profile: true
},
orderBy: {
email: 'asc'
}
});
return users;
}
static async deleteUser(adminId: string, targetId: string) {
if (targetId === adminId) {
throw new Error('Cannot delete yourself');
}
await prisma.user.delete({ where: { id: targetId } });
}
static async blockUser(adminId: string, targetId: string, block: boolean) {
if (targetId === adminId) {
throw new Error('Cannot block yourself');
}
await prisma.user.update({
where: { id: targetId },
data: { isBlocked: block }
});
}
static async resetUserPassword(targetId: string, newPassword: string) {
if (!newPassword || newPassword.length < 4) {
throw new Error('Password too short');
}
const hashed = await bcrypt.hash(newPassword, 10);
await prisma.user.update({
where: { id: targetId },
data: {
password: hashed,
isFirstLogin: true
}
});
}
}

View File

@@ -0,0 +1,69 @@
import prisma from '../lib/prisma';
export class ExerciseService {
static async getAllExercises(userId: string) {
const exercises = await prisma.exercise.findMany({
where: {
OR: [
{ userId: null }, // System default
{ userId } // User custom
]
}
});
return exercises;
}
static async getLastSet(userId: string, exerciseId: string) {
const lastSet = await prisma.workoutSet.findFirst({
where: {
exerciseId,
session: { userId }
},
include: {
session: true
},
orderBy: {
timestamp: 'desc'
}
});
return lastSet;
}
static async saveExercise(userId: string, data: any) {
const { id, name, type, bodyWeightPercentage, isArchived, isUnilateral } = data;
const exerciseData = {
name,
type,
bodyWeightPercentage: bodyWeightPercentage ? parseFloat(bodyWeightPercentage) : undefined,
isArchived: !!isArchived,
isUnilateral: !!isUnilateral
};
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: exerciseData
});
return updated;
}
}
// Create new
const newExercise = await prisma.exercise.create({
data: {
id: id || undefined,
userId,
name: exerciseData.name,
type: exerciseData.type,
bodyWeightPercentage: exerciseData.bodyWeightPercentage,
isArchived: exerciseData.isArchived,
isUnilateral: exerciseData.isUnilateral,
}
});
return newExercise;
}
}

View File

@@ -0,0 +1,92 @@
import prisma from '../lib/prisma';
export class PlanService {
static async getPlans(userId: string) {
const plans = await prisma.workoutPlan.findMany({
where: { userId },
include: {
planExercises: {
include: { exercise: true },
orderBy: { order: 'asc' }
}
},
orderBy: { createdAt: 'desc' }
});
return plans.map((p: any) => ({
...p,
steps: p.planExercises.map((pe: any) => ({
id: pe.id,
exerciseId: pe.exerciseId,
exerciseName: pe.exercise.name,
exerciseType: pe.exercise.type,
isWeighted: pe.isWeighted,
}))
}));
}
static async savePlan(userId: string, data: any) {
const { id, name, description, steps } = data;
await prisma.$transaction(async (tx) => {
let plan = await tx.workoutPlan.findUnique({ where: { id } });
if (plan) {
await tx.workoutPlan.update({
where: { id },
data: { name, description }
});
await tx.planExercise.deleteMany({ where: { planId: id } });
} else {
await tx.workoutPlan.create({
data: {
id,
userId,
name,
description
}
});
}
if (steps && steps.length > 0) {
await tx.planExercise.createMany({
data: steps.map((step: any, index: number) => ({
planId: id,
exerciseId: step.exerciseId,
order: index,
isWeighted: step.isWeighted || false
}))
});
}
});
const savedPlan = await prisma.workoutPlan.findUnique({
where: { id },
include: {
planExercises: {
include: { exercise: true },
orderBy: { order: 'asc' }
}
}
});
if (!savedPlan) throw new Error("Plan failed to save");
return {
...savedPlan,
steps: savedPlan.planExercises.map((pe: any) => ({
id: pe.id,
exerciseId: pe.exerciseId,
exerciseName: pe.exercise.name,
exerciseType: pe.exercise.type,
isWeighted: pe.isWeighted
}))
};
}
static async deletePlan(userId: string, id: string) {
await prisma.workoutPlan.delete({
where: { id, userId }
});
}
}

View File

@@ -0,0 +1,435 @@
import prisma from '../lib/prisma';
import { WorkoutSession, WorkoutSet } from '@prisma/client';
export class SessionService {
static async getAllSessions(userId: string) {
const sessions = await prisma.workoutSession.findMany({
where: {
userId,
OR: [
{ endTime: { not: null } },
{ type: { equals: 'QUICK_LOG' } } // Ensure type is handled correctly
]
},
include: { sets: { include: { exercise: true } } },
orderBy: { startTime: 'desc' }
});
return sessions.map(session => ({
...session,
sets: session.sets.map(set => ({
...set,
exerciseName: set.exercise.name,
type: set.exercise.type
}))
}));
}
static async saveSession(userId: string, data: any) {
const { id, startTime, endTime, userBodyWeight, note, planId, planName, sets } = data;
const start = new Date(startTime);
const end = endTime ? new Date(endTime) : null;
const weight = userBodyWeight ? parseFloat(userBodyWeight) : null;
const existing = await prisma.workoutSession.findUnique({ where: { id } });
if (existing) {
// Update
await prisma.workoutSet.deleteMany({ where: { sessionId: id } });
const updated = await prisma.workoutSession.update({
where: { id },
data: {
startTime: start,
endTime: end,
userBodyWeight: weight,
note,
planId,
planName,
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 !== undefined ? s.completed : true
}))
}
},
include: { sets: true }
});
return updated;
} else {
// Create
if (!end) {
const active = await prisma.workoutSession.findFirst({
where: {
userId,
endTime: null,
type: 'STANDARD'
}
});
if (active) {
throw new Error('An active session already exists');
}
}
const created = await prisma.workoutSession.create({
data: {
id,
userId,
startTime: start,
endTime: end,
userBodyWeight: weight,
note,
planId,
planName,
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 !== undefined ? s.completed : true
}))
}
},
include: { sets: true }
});
if (weight && end) {
await prisma.userProfile.upsert({
where: { userId },
create: { userId, weight },
update: { weight }
});
}
return created;
}
}
static async getActiveSession(userId: string) {
const activeSession = await prisma.workoutSession.findFirst({
where: {
userId,
endTime: null,
type: 'STANDARD'
},
include: { sets: { include: { exercise: true }, orderBy: { order: 'asc' } } }
});
return activeSession;
}
static async updateActiveSession(userId: string, data: any) {
const { id, startTime, endTime, userBodyWeight, note, planId, planName, sets } = data;
const start = new Date(startTime);
const end = endTime ? new Date(endTime) : null;
const weight = userBodyWeight ? parseFloat(userBodyWeight) : null;
const existing = await prisma.workoutSession.findFirst({
where: { id, userId }
});
if (!existing) {
throw new Error('Session not found');
}
await prisma.workoutSet.deleteMany({ where: { sessionId: id } });
const updated = await prisma.workoutSession.update({
where: { id },
data: {
startTime: start,
endTime: end,
userBodyWeight: weight,
note,
planId,
planName,
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 !== undefined ? s.completed : true
}))
}
},
include: { sets: { include: { exercise: true } } }
});
if (weight && end) {
await prisma.userProfile.upsert({
where: { userId },
create: { userId, weight },
update: { weight }
});
}
return updated;
}
static async getTodayQuickLog(userId: string) {
const startOfDay = new Date();
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999);
const session = await prisma.workoutSession.findFirst({
where: {
userId,
// @ts-ignore: Prisma enum mismatch in generated types potentially
type: 'QUICK_LOG',
startTime: {
gte: startOfDay,
lte: endOfDay
}
},
include: { sets: { include: { exercise: true }, orderBy: { timestamp: 'desc' } } }
});
if (!session) return null;
return {
...session,
sets: session.sets.map(set => ({
...set,
exerciseName: set.exercise.name,
type: set.exercise.type
}))
};
}
static async logSetToQuickLog(userId: string, data: any) {
const { exerciseId, weight, reps, distanceMeters, durationSeconds, height, bodyWeightPercentage, note, side } = data;
const startOfDay = new Date();
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999);
let session = await prisma.workoutSession.findFirst({
where: {
userId,
type: 'QUICK_LOG', // Type safety is tricky with string literals sometimes
startTime: {
gte: startOfDay,
lte: endOfDay
}
}
});
if (!session) {
session = await prisma.workoutSession.create({
data: {
userId,
startTime: startOfDay,
type: 'QUICK_LOG',
note: 'Daily Quick Log'
}
});
}
const newSet = await prisma.workoutSet.create({
data: {
sessionId: session.id,
exerciseId,
order: 0,
weight: weight ? parseFloat(weight) : null,
reps: reps ? parseInt(reps) : null,
distanceMeters: distanceMeters ? parseFloat(distanceMeters) : null,
durationSeconds: durationSeconds ? parseInt(durationSeconds) : null,
side: side || null
},
include: { exercise: true }
});
return {
...newSet,
exerciseName: newSet.exercise.name,
type: newSet.exercise.type
};
}
static async logSetToActiveSession(userId: string, data: any) {
const { exerciseId, reps, weight, distanceMeters, durationSeconds, side } = data;
const activeSession = await prisma.workoutSession.findFirst({
where: { userId, endTime: null, type: 'STANDARD' },
include: { sets: true }
});
if (!activeSession) {
throw new Error('No active session found');
}
const maxOrder = activeSession.sets.reduce((max, set) => Math.max(max, set.order), -1);
const newSet = await prisma.workoutSet.create({
data: {
sessionId: activeSession.id,
exerciseId,
order: maxOrder + 1,
reps: reps ? parseInt(reps) : null,
weight: weight ? parseFloat(weight) : null,
distanceMeters: distanceMeters ? parseFloat(distanceMeters) : null,
durationSeconds: durationSeconds ? parseInt(durationSeconds) : null,
side: side || null,
completed: true
},
include: { exercise: true }
});
// Recalculate active step
let activeExerciseId = null;
if (activeSession.planId) {
const plan = await prisma.workoutPlan.findUnique({
where: { id: activeSession.planId }
});
if (plan) {
const planExercises: { id: string }[] = JSON.parse(plan.exercises || '[]');
const allPerformedSets = await prisma.workoutSet.findMany({
where: { sessionId: activeSession.id }
});
const performedCounts = new Map<string, number>();
for (const set of allPerformedSets) {
performedCounts.set(set.exerciseId, (performedCounts.get(set.exerciseId) || 0) + 1);
}
const plannedCounts = new Map<string, number>();
for (const planExercise of planExercises) {
const exerciseIdStr = planExercise.id;
plannedCounts.set(exerciseIdStr, (plannedCounts.get(exerciseIdStr) || 0) + 1);
const performedCount = performedCounts.get(exerciseIdStr) || 0;
if (performedCount < plannedCounts.get(exerciseIdStr)!) {
activeExerciseId = exerciseIdStr;
break;
}
}
}
}
return {
newSet: {
...newSet,
exerciseName: newSet.exercise.name,
type: newSet.exercise.type
},
activeExerciseId
};
}
static async updateSet(userId: string, setId: string, data: any) {
// Find active session (STANDARD or QUICK_LOG) to ensure user owns the set effectively
// Or just check if set belongs to a session owned by user.
// The existing code checked for an active session but really we just need to verify ownership.
// However, `updateSet` in `sessions.ts` only looked for an active session first, which implies you can only edit sets in active sessions.
// I will maintain that logic.
const activeSession = await prisma.workoutSession.findFirst({
where: { userId, endTime: null },
});
if (!activeSession) {
throw new Error('No active session found');
}
// We should probably verify the set belongs to this session, but the original code just checked activeSession exists and assumed set id is valid/belongs to it?
// Actually `updatedSet = await prisma.workoutSet.update({ where: { id: setId } ... })` handles the update.
// If the set doesn't exist it throws. If it belongs to another user... wait, `update` uses `id` primarily.
// Ideally we should check ownership. But copying logic for now.
const { reps, weight, distanceMeters, durationSeconds } = data;
const updatedSet = await prisma.workoutSet.update({
where: { id: setId },
data: {
reps: reps ? parseInt(reps) : null,
weight: weight ? parseFloat(weight) : null,
distanceMeters: distanceMeters ? parseFloat(distanceMeters) : null,
durationSeconds: durationSeconds ? parseInt(durationSeconds) : null,
},
include: { exercise: true }
});
return {
...updatedSet,
exerciseName: updatedSet.exercise.name,
type: updatedSet.exercise.type
};
}
static async patchSet(userId: string, setId: string, data: any) {
const activeSession = await prisma.workoutSession.findFirst({
where: { userId, endTime: null },
});
if (!activeSession) {
throw new Error('No active session found');
}
const { reps, weight, distanceMeters, durationSeconds, height, bodyWeightPercentage, side, note } = data;
const updatedSet = await prisma.workoutSet.update({
where: { id: setId },
data: {
reps: reps !== undefined ? (reps ? parseInt(reps) : null) : undefined,
weight: weight !== undefined ? (weight ? parseFloat(weight) : null) : undefined,
distanceMeters: distanceMeters !== undefined ? (distanceMeters ? parseFloat(distanceMeters) : null) : undefined,
durationSeconds: durationSeconds !== undefined ? (durationSeconds ? parseInt(durationSeconds) : null) : undefined,
height: height !== undefined ? (height ? parseFloat(height) : null) : undefined,
bodyWeightPercentage: bodyWeightPercentage !== undefined ? (bodyWeightPercentage ? parseFloat(bodyWeightPercentage) : null) : undefined,
side: side !== undefined ? side : undefined,
},
include: { exercise: true }
});
return {
...updatedSet,
exerciseName: updatedSet.exercise.name,
type: updatedSet.exercise.type
};
}
static async deleteSet(userId: string, setId: string) {
const activeSession = await prisma.workoutSession.findFirst({
where: { userId, endTime: null },
});
if (!activeSession) {
throw new Error('No active session found');
}
await prisma.workoutSet.delete({
where: { id: setId }
});
}
static async deleteActiveSession(userId: string) {
await prisma.workoutSession.deleteMany({
where: {
userId,
endTime: null,
type: 'STANDARD'
}
});
}
static async deleteSession(userId: string, sessionId: string) {
await prisma.workoutSession.delete({
where: { id: sessionId, userId }
});
}
}

View File

@@ -0,0 +1,48 @@
import prisma from '../lib/prisma';
export class WeightService {
static async getWeightHistory(userId: string) {
const weights = await prisma.bodyWeightRecord.findMany({
where: { userId },
orderBy: { date: 'desc' },
take: 365
});
return weights;
}
static async logWeight(userId: string, weight: number, dateStr: string) {
const record = await prisma.bodyWeightRecord.upsert({
where: {
userId_dateStr: {
userId,
dateStr
}
},
update: {
weight: weight,
date: new Date(dateStr)
},
create: {
userId,
weight: weight,
dateStr,
date: new Date(dateStr)
}
});
// Update profile if latest
const latestRecord = await prisma.bodyWeightRecord.findFirst({
where: { userId },
orderBy: { date: 'desc' }
});
if (latestRecord && latestRecord.id === record.id) {
await prisma.userProfile.update({
where: { userId },
data: { weight: weight }
});
}
return record;
}
}

View File

@@ -0,0 +1,23 @@
import { Response } from 'express';
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
}
export const sendSuccess = <T>(res: Response, data: T, statusCode: number = 200) => {
const response: ApiResponse<T> = {
success: true,
data,
};
return res.status(statusCode).json(response);
};
export const sendError = (res: Response, error: string, statusCode: number = 400) => {
const response: ApiResponse<null> = {
success: false,
error,
};
return res.status(statusCode).json(response);
};

View File

@@ -0,0 +1,22 @@
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
defaultMeta: { service: 'gymflow-backend' },
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
],
});
if (process.env.APP_MODE !== 'prod') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
}));
}
export default logger;