1. Tailwind migretion. 2. Backend Type Safety. 3. Context Refactoring.
This commit is contained in:
12
server/package-lock.json
generated
12
server/package-lock.json
generated
@@ -18,7 +18,8 @@
|
||||
"dotenv": "17.2.3",
|
||||
"express": "5.1.0",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"ts-node-dev": "^2.0.0"
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"zod": "^4.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "*",
|
||||
@@ -3454,6 +3455,15 @@
|
||||
"dependencies": {
|
||||
"grammex": "^3.1.10"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz",
|
||||
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"dotenv": "17.2.3",
|
||||
"express": "5.1.0",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"ts-node-dev": "^2.0.0"
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"zod": "^4.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "*",
|
||||
@@ -37,4 +38,4 @@
|
||||
"ts-node": "*",
|
||||
"typescript": "*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
15
server/src/middleware/validate.ts
Normal file
15
server/src/middleware/validate.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ZodSchema } from 'zod';
|
||||
|
||||
export const validate = (schema: ZodSchema<any>) => async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await schema.parseAsync({
|
||||
body: req.body,
|
||||
query: req.query,
|
||||
params: req.params,
|
||||
});
|
||||
return next();
|
||||
} catch (error) {
|
||||
return res.status(400).json(error);
|
||||
}
|
||||
};
|
||||
@@ -13,8 +13,8 @@ interface AuthRequest extends Request {
|
||||
|
||||
const router = express.Router();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
|
||||
const API_KEY = process.env.API_KEY;
|
||||
const MODEL_ID = 'gemini-2.0-flash';
|
||||
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>();
|
||||
|
||||
@@ -2,6 +2,8 @@ import express from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import prisma from '../lib/prisma';
|
||||
import { validate } from '../middleware/validate';
|
||||
import { loginSchema, registerSchema, changePasswordSchema, updateProfileSchema } from '../schemas/auth';
|
||||
|
||||
const router = express.Router();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
|
||||
@@ -30,7 +32,7 @@ router.get('/me', async (req, res) => {
|
||||
});
|
||||
|
||||
// Login
|
||||
router.post('/login', async (req, res) => {
|
||||
router.post('/login', validate(loginSchema), async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
@@ -63,7 +65,7 @@ router.post('/login', async (req, res) => {
|
||||
});
|
||||
|
||||
// Register
|
||||
router.post('/register', async (req, res) => {
|
||||
router.post('/register', validate(registerSchema), async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
@@ -73,10 +75,6 @@ router.post('/register', async (req, res) => {
|
||||
return res.status(400).json({ error: 'User already exists' });
|
||||
}
|
||||
|
||||
if (!password || password.length < 4) {
|
||||
return res.status(400).json({ error: 'Password too short' });
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
const user = await prisma.user.create({
|
||||
@@ -104,7 +102,7 @@ router.post('/register', async (req, res) => {
|
||||
});
|
||||
|
||||
// Change Password
|
||||
router.post('/change-password', async (req, res) => {
|
||||
router.post('/change-password', validate(changePasswordSchema), async (req, res) => {
|
||||
|
||||
try {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
@@ -118,10 +116,6 @@ router.post('/change-password', async (req, res) => {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
|
||||
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({
|
||||
@@ -140,7 +134,7 @@ router.post('/change-password', async (req, res) => {
|
||||
});
|
||||
|
||||
// Update Profile
|
||||
router.patch('/profile', async (req, res) => {
|
||||
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' });
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import express from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import prisma from '../lib/prisma';
|
||||
import { validate } from '../middleware/validate';
|
||||
import { sessionSchema, logSetSchema, updateSetSchema } from '../schemas/sessions';
|
||||
|
||||
const router = express.Router();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
|
||||
@@ -53,7 +55,7 @@ router.get('/', async (req: any, res) => {
|
||||
});
|
||||
|
||||
// Save session (create or update)
|
||||
router.post('/', async (req: any, res) => {
|
||||
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;
|
||||
@@ -148,15 +150,6 @@ router.post('/', async (req: any, res) => {
|
||||
return res.json(created);
|
||||
}
|
||||
|
||||
// Update user profile weight if session has weight and is finished (for update case too)
|
||||
if (weight && end) {
|
||||
await prisma.userProfile.upsert({
|
||||
where: { userId },
|
||||
create: { userId, weight },
|
||||
update: { weight }
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
@@ -188,7 +181,7 @@ router.get('/active', async (req: any, res) => {
|
||||
});
|
||||
|
||||
// Update active session (for real-time set updates)
|
||||
router.put('/active', async (req: any, res) => {
|
||||
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;
|
||||
@@ -293,7 +286,7 @@ router.get('/quick-log', async (req: any, res) => {
|
||||
});
|
||||
|
||||
// Log a set to today's quick log session
|
||||
router.post('/quick-log/set', async (req: any, res) => {
|
||||
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;
|
||||
@@ -355,7 +348,7 @@ router.post('/quick-log/set', async (req: any, res) => {
|
||||
});
|
||||
|
||||
// Log a set to the active session
|
||||
router.post('/active/log-set', async (req: any, res) => {
|
||||
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;
|
||||
@@ -486,7 +479,7 @@ router.put('/active/set/:setId', async (req: any, res) => {
|
||||
});
|
||||
|
||||
// Update a set in the active session (STANDARD or QUICK_LOG)
|
||||
router.patch('/active/set/:setId', async (req: any, res) => {
|
||||
router.patch('/active/set/:setId', validate(updateSetSchema), async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const { setId } = req.params;
|
||||
@@ -591,113 +584,4 @@ router.delete('/:id', async (req: any, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 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 exercise properties to sets for frontend compatibility
|
||||
const mappedSession = {
|
||||
...session,
|
||||
sets: session.sets.map((set: any) => ({
|
||||
...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', 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, // Order not strictly enforced for quick log
|
||||
weight: weight ? parseFloat(weight) : null,
|
||||
reps: reps ? parseInt(reps) : null,
|
||||
distanceMeters: distanceMeters ? parseFloat(distanceMeters) : null,
|
||||
durationSeconds: durationSeconds ? parseInt(durationSeconds) : null,
|
||||
height: height ? parseFloat(height) : null,
|
||||
bodyWeightPercentage: bodyWeightPercentage ? parseFloat(bodyWeightPercentage) : null,
|
||||
side: side || null,
|
||||
completed: true,
|
||||
timestamp: new Date()
|
||||
},
|
||||
include: { exercise: true }
|
||||
});
|
||||
|
||||
const mappedSet = {
|
||||
...newSet,
|
||||
exerciseName: newSet.exercise.name,
|
||||
type: newSet.exercise.type
|
||||
};
|
||||
|
||||
res.json({ success: true, newSet: mappedSet });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
32
server/src/schemas/auth.ts
Normal file
32
server/src/schemas/auth.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const loginSchema = z.object({
|
||||
body: z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(1),
|
||||
}),
|
||||
});
|
||||
|
||||
export const registerSchema = z.object({
|
||||
body: z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(4),
|
||||
role: z.enum(['USER', 'ADMIN']).optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const changePasswordSchema = z.object({
|
||||
body: z.object({
|
||||
newPassword: z.string().min(4),
|
||||
}),
|
||||
});
|
||||
|
||||
export const updateProfileSchema = z.object({
|
||||
body: z.object({
|
||||
weight: z.number().optional(),
|
||||
height: z.number().optional(),
|
||||
gender: z.string().optional(),
|
||||
birthDate: z.string().optional(),
|
||||
language: z.string().optional()
|
||||
})
|
||||
})
|
||||
47
server/src/schemas/sessions.ts
Normal file
47
server/src/schemas/sessions.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const sessionSchema = z.object({
|
||||
body: z.object({
|
||||
startTime: z.union([z.number(), z.string(), z.date()]), // Date.now or ISO string
|
||||
endTime: z.union([z.number(), z.string(), z.date()]).nullable().optional(),
|
||||
userBodyWeight: z.number().nullable().optional(),
|
||||
note: z.string().nullable().optional(),
|
||||
planId: z.string().nullable().optional(),
|
||||
planName: z.string().nullable().optional(),
|
||||
sets: z.array(z.object({
|
||||
exerciseId: z.string(),
|
||||
weight: z.number().nullable().optional(),
|
||||
reps: z.number().nullable().optional(),
|
||||
distanceMeters: z.number().nullable().optional(),
|
||||
durationSeconds: z.number().nullable().optional(),
|
||||
completed: z.boolean().optional().default(true),
|
||||
side: z.string().nullable().optional()
|
||||
}))
|
||||
})
|
||||
});
|
||||
|
||||
export const logSetSchema = z.object({
|
||||
body: z.object({
|
||||
exerciseId: z.string(),
|
||||
weight: z.number().nullable().optional(),
|
||||
reps: z.number().nullable().optional(),
|
||||
distanceMeters: z.number().nullable().optional(),
|
||||
durationSeconds: z.number().nullable().optional(),
|
||||
height: z.number().nullable().optional(),
|
||||
bodyWeightPercentage: z.number().nullable().optional(),
|
||||
note: z.string().nullable().optional(),
|
||||
side: z.string().nullable().optional(),
|
||||
})
|
||||
});
|
||||
|
||||
export const updateSetSchema = z.object({
|
||||
body: z.object({
|
||||
weight: z.number().nullable().optional(),
|
||||
reps: z.number().nullable().optional(),
|
||||
distanceMeters: z.number().nullable().optional(),
|
||||
durationSeconds: z.number().nullable().optional(),
|
||||
height: z.number().nullable().optional(),
|
||||
bodyWeightPercentage: z.number().nullable().optional(),
|
||||
side: z.string().nullable().optional(),
|
||||
})
|
||||
});
|
||||
Reference in New Issue
Block a user