1. Tailwind migretion. 2. Backend Type Safety. 3. Context Refactoring.

This commit is contained in:
AG
2025-12-07 21:54:32 +02:00
parent e893336d46
commit 57f7ad077e
27 changed files with 1536 additions and 580 deletions

View File

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

View File

@@ -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.

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

View File

@@ -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>();

View File

@@ -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' });

View File

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

View 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()
})
})

View 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(),
})
});