Compare commits
1 Commits
48446b483c
...
feat/phase
| Author | SHA1 | Date | |
|---|---|---|---|
| 4646c6ed1a |
@@ -8,7 +8,8 @@
|
||||
"start": "node dist/index.js",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:generate": "prisma generate",
|
||||
"db:studio": "prisma studio"
|
||||
"db:studio": "prisma studio",
|
||||
"db:seed": "ts-node src/db/seed.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
||||
48
backend/src/controllers/exercise.controller.ts
Normal file
48
backend/src/controllers/exercise.controller.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { Response } from "express";
|
||||
import { prisma } from "../index";
|
||||
import type { AppRequest } from "../types/context";
|
||||
import { createExerciseSchema, updateExerciseSchema } from "../validators/exercise.validators";
|
||||
|
||||
export async function getAll(_req: AppRequest, res: Response): Promise<void> {
|
||||
const exercises = await prisma.exercise.findMany({
|
||||
orderBy: { name: "asc" },
|
||||
});
|
||||
res.json(exercises);
|
||||
}
|
||||
|
||||
export async function getOne(req: AppRequest, res: Response): Promise<void> {
|
||||
const id = req.params.id as string;
|
||||
const exercise = await prisma.exercise.findUnique({ where: { id } });
|
||||
if (!exercise) {
|
||||
res.status(404).json({ message: "Exercice introuvable." });
|
||||
return;
|
||||
}
|
||||
res.json(exercise);
|
||||
}
|
||||
|
||||
export async function create(req: AppRequest, res: Response): Promise<void> {
|
||||
const parsed = createExerciseSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||
return;
|
||||
}
|
||||
const exercise = await prisma.exercise.create({ data: parsed.data });
|
||||
res.status(201).json(exercise);
|
||||
}
|
||||
|
||||
export async function update(req: AppRequest, res: Response): Promise<void> {
|
||||
const id = req.params.id as string;
|
||||
const parsed = updateExerciseSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||
return;
|
||||
}
|
||||
const exercise = await prisma.exercise.update({ where: { id }, data: parsed.data });
|
||||
res.json(exercise);
|
||||
}
|
||||
|
||||
export async function remove(req: AppRequest, res: Response): Promise<void> {
|
||||
const id = req.params.id as string;
|
||||
await prisma.exercise.delete({ where: { id } });
|
||||
res.status(204).send();
|
||||
}
|
||||
123
backend/src/controllers/program.controller.ts
Normal file
123
backend/src/controllers/program.controller.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { Response } from "express";
|
||||
import { prisma } from "../index";
|
||||
import type { AppRequest } from "../types/context";
|
||||
import { createProgramSchema, updateProgramSchema } from "../validators/program.validators";
|
||||
|
||||
export async function getAll(req: AppRequest, res: Response): Promise<void> {
|
||||
const userId = req.user?.id;
|
||||
const programs = await prisma.program.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ isPublic: true },
|
||||
{ authorId: userId },
|
||||
],
|
||||
},
|
||||
include: {
|
||||
author: { select: { id: true, username: true, avatar: true } },
|
||||
exercises: {
|
||||
include: { exercise: true },
|
||||
orderBy: { order: "asc" },
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
res.json(programs);
|
||||
}
|
||||
|
||||
export async function getOne(req: AppRequest, res: Response): Promise<void> {
|
||||
const id = req.params.id as string;
|
||||
const userId = req.user?.id;
|
||||
const program = await prisma.program.findFirst({
|
||||
where: {
|
||||
id,
|
||||
OR: [{ isPublic: true }, { authorId: userId }],
|
||||
},
|
||||
include: {
|
||||
author: { select: { id: true, username: true, avatar: true } },
|
||||
exercises: {
|
||||
include: { exercise: true },
|
||||
orderBy: { order: "asc" },
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!program) {
|
||||
res.status(404).json({ message: "Programme introuvable." });
|
||||
return;
|
||||
}
|
||||
res.json(program);
|
||||
}
|
||||
|
||||
export async function create(req: AppRequest, res: Response): Promise<void> {
|
||||
const parsed = createProgramSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||
return;
|
||||
}
|
||||
|
||||
const { exercises, ...programData } = parsed.data;
|
||||
|
||||
const program = await prisma.program.create({
|
||||
data: {
|
||||
...programData,
|
||||
authorId: req.user!.id,
|
||||
exercises: {
|
||||
create: exercises.map(({ exerciseId, sets, reps, durationSec, order }) => ({
|
||||
exerciseId,
|
||||
sets,
|
||||
reps,
|
||||
durationSec,
|
||||
order,
|
||||
})),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
exercises: {
|
||||
include: { exercise: true },
|
||||
orderBy: { order: "asc" },
|
||||
},
|
||||
},
|
||||
});
|
||||
res.status(201).json(program);
|
||||
}
|
||||
|
||||
export async function update(req: AppRequest, res: Response): Promise<void> {
|
||||
const id = req.params.id as string;
|
||||
const parsed = updateProgramSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||
return;
|
||||
}
|
||||
|
||||
const program = await prisma.program.findFirst({
|
||||
where: { id, authorId: req.user!.id },
|
||||
});
|
||||
if (!program) {
|
||||
res.status(404).json({ message: "Programme introuvable." });
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await prisma.program.update({
|
||||
where: { id },
|
||||
data: parsed.data,
|
||||
include: {
|
||||
exercises: {
|
||||
include: { exercise: true },
|
||||
orderBy: { order: "asc" },
|
||||
},
|
||||
},
|
||||
});
|
||||
res.json(updated);
|
||||
}
|
||||
|
||||
export async function remove(req: AppRequest, res: Response): Promise<void> {
|
||||
const id = req.params.id as string;
|
||||
const program = await prisma.program.findFirst({
|
||||
where: { id, authorId: req.user!.id },
|
||||
});
|
||||
if (!program) {
|
||||
res.status(404).json({ message: "Programme introuvable." });
|
||||
return;
|
||||
}
|
||||
await prisma.program.delete({ where: { id } });
|
||||
res.status(204).send();
|
||||
}
|
||||
106
backend/src/db/seed.ts
Normal file
106
backend/src/db/seed.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import "dotenv/config";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import argon2 from "argon2";
|
||||
|
||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
async function main() {
|
||||
console.log("🌱 Seed en cours...");
|
||||
|
||||
// ── Admin ──────────────────────────────────────────────────────────────────
|
||||
const adminPassword = await argon2.hash("admin1234");
|
||||
const admin = await prisma.user.upsert({
|
||||
where: { email: "admin@pulseform.app" },
|
||||
update: {},
|
||||
create: {
|
||||
username: "admin",
|
||||
email: "admin@pulseform.app",
|
||||
password: adminPassword,
|
||||
role: "ADMIN",
|
||||
},
|
||||
});
|
||||
console.log(`✅ Admin : ${admin.email}`);
|
||||
|
||||
// ── Exercices ──────────────────────────────────────────────────────────────
|
||||
const exercisesData = [
|
||||
{ name: "Squat", description: "Flexion des jambes, dos droit.", difficulty: "BEGINNER" as const, muscleGroups: ["Legs", "Glutes"] },
|
||||
{ name: "Squat sauté", description: "Squat avec impulsion vers le haut.", difficulty: "INTERMEDIATE" as const, muscleGroups: ["Legs", "Glutes"] },
|
||||
{ name: "Fente avant", description: "Pas en avant, genou à 90°.", difficulty: "BEGINNER" as const, muscleGroups: ["Legs", "Glutes"] },
|
||||
{ name: "Pompes", description: "Bras à largeur d'épaules, corps droit.", difficulty: "BEGINNER" as const, muscleGroups: ["Chest", "Arms"] },
|
||||
{ name: "Pompes diamant", description: "Mains proches, triceps sollicités.", difficulty: "INTERMEDIATE" as const, muscleGroups: ["Arms", "Chest"] },
|
||||
{ name: "Dips", description: "Sur une chaise ou barre parallèle.", difficulty: "INTERMEDIATE" as const, muscleGroups: ["Arms", "Chest"] },
|
||||
{ name: "Traction", description: "Barre fixe, prise pronation.", difficulty: "ADVANCED" as const, muscleGroups: ["Back", "Arms"] },
|
||||
{ name: "Rowing incliné", description: "Haltère, dos à 45°.", difficulty: "INTERMEDIATE" as const, muscleGroups: ["Back"] },
|
||||
{ name: "Planche", description: "Corps droit, abdos contractés.", difficulty: "BEGINNER" as const, muscleGroups: ["Abdominals"] },
|
||||
{ name: "Crunch", description: "Soulever les épaules, pas le dos.", difficulty: "BEGINNER" as const, muscleGroups: ["Abdominals"] },
|
||||
{ name: "Mountain climber", description: "Alterner genoux vers la poitrine.", difficulty: "INTERMEDIATE" as const, muscleGroups: ["Abdominals", "Legs"] },
|
||||
{ name: "Burpee", description: "Pompe + saut, exercice full body.", difficulty: "ADVANCED" as const, muscleGroups: ["Legs", "Chest", "Arms"] },
|
||||
];
|
||||
|
||||
await prisma.exercise.createMany({ data: exercisesData, skipDuplicates: true });
|
||||
const exercises = await prisma.exercise.findMany();
|
||||
console.log(`✅ ${exercises.length} exercices`);
|
||||
|
||||
// ── Programme débutant ─────────────────────────────────────────────────────
|
||||
const squat = exercises.find((e) => e.name === "Squat")!;
|
||||
const pompes = exercises.find((e) => e.name === "Pompes")!;
|
||||
const planche = exercises.find((e) => e.name === "Planche")!;
|
||||
const crunch = exercises.find((e) => e.name === "Crunch")!;
|
||||
const fente = exercises.find((e) => e.name === "Fente avant")!;
|
||||
|
||||
await prisma.program.upsert({
|
||||
where: { id: "00000000-0000-0000-0000-000000000001" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "00000000-0000-0000-0000-000000000001",
|
||||
name: "Full Body Débutant",
|
||||
description: "Programme complet pour commencer sans équipement.",
|
||||
isPublic: true,
|
||||
authorId: admin.id,
|
||||
exercises: {
|
||||
create: [
|
||||
{ exerciseId: squat.id!, sets: 3, reps: 12, order: 0 },
|
||||
{ exerciseId: pompes.id!, sets: 3, reps: 10, order: 1 },
|
||||
{ exerciseId: fente.id!, sets: 3, reps: 10, order: 2 },
|
||||
{ exerciseId: planche.id!, sets: 3, durationSec: 30, order: 3 },
|
||||
{ exerciseId: crunch.id!, sets: 3, reps: 15, order: 4 },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log("✅ Programme : Full Body Débutant");
|
||||
|
||||
// ── Programme intermédiaire ────────────────────────────────────────────────
|
||||
const burpee = exercises.find((e) => e.name === "Burpee")!;
|
||||
const mountain = exercises.find((e) => e.name === "Mountain climber")!;
|
||||
const squatSaute = exercises.find((e) => e.name === "Squat sauté")!;
|
||||
|
||||
await prisma.program.upsert({
|
||||
where: { id: "00000000-0000-0000-0000-000000000002" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "00000000-0000-0000-0000-000000000002",
|
||||
name: "Cardio Intermédiaire",
|
||||
description: "Circuit cardio sans équipement, intensité modérée.",
|
||||
isPublic: true,
|
||||
authorId: admin.id,
|
||||
exercises: {
|
||||
create: [
|
||||
{ exerciseId: burpee.id!, sets: 4, reps: 8, order: 0 },
|
||||
{ exerciseId: squatSaute.id!, sets: 4, reps: 12, order: 1 },
|
||||
{ exerciseId: mountain.id!, sets: 4, durationSec: 30, order: 2 },
|
||||
{ exerciseId: pompes.id!, sets: 4, reps: 12, order: 3 },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log("✅ Programme : Cardio Intermédiaire");
|
||||
|
||||
console.log("🎉 Seed terminé.");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => { console.error(e); process.exit(1); })
|
||||
.finally(() => prisma.$disconnect());
|
||||
@@ -6,6 +6,8 @@ import { PrismaClient } from "@prisma/client";
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import authRoutes from "./routes/auth.routes";
|
||||
import userRoutes from "./routes/user.routes";
|
||||
import exerciseRoutes from "./routes/exercise.routes";
|
||||
import programRoutes from "./routes/program.routes";
|
||||
|
||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||
const app = express();
|
||||
@@ -32,8 +34,8 @@ app.use("/uploads", express.static("uploads"));
|
||||
|
||||
app.use("/api/auth", authRoutes);
|
||||
app.use("/api/users", userRoutes);
|
||||
// app.use("/api/exercises", exerciseRoutes);
|
||||
// app.use("/api/programs", programRoutes);
|
||||
app.use("/api/exercises", exerciseRoutes);
|
||||
app.use("/api/programs", programRoutes);
|
||||
// app.use("/api/groups", groupRoutes);
|
||||
// app.use("/api/friends", friendRoutes);
|
||||
// app.use("/api/history", historyRoutes);
|
||||
|
||||
13
backend/src/routes/exercise.routes.ts
Normal file
13
backend/src/routes/exercise.routes.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Router } from "express";
|
||||
import { getAll, getOne, create, update, remove } from "../controllers/exercise.controller";
|
||||
import { requireAuth, requireRole } from "../middlewares/auth";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", requireAuth, getAll);
|
||||
router.get("/:id", requireAuth, getOne);
|
||||
router.post("/", requireAuth, requireRole("ADMIN", "COACH"), create);
|
||||
router.patch("/:id", requireAuth, requireRole("ADMIN", "COACH"), update);
|
||||
router.delete("/:id", requireAuth, requireRole("ADMIN"), remove);
|
||||
|
||||
export default router;
|
||||
13
backend/src/routes/program.routes.ts
Normal file
13
backend/src/routes/program.routes.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Router } from "express";
|
||||
import { getAll, getOne, create, update, remove } from "../controllers/program.controller";
|
||||
import { requireAuth } from "../middlewares/auth";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", requireAuth, getAll);
|
||||
router.get("/:id", requireAuth, getOne);
|
||||
router.post("/", requireAuth, create);
|
||||
router.patch("/:id", requireAuth, update);
|
||||
router.delete("/:id", requireAuth, remove);
|
||||
|
||||
export default router;
|
||||
14
backend/src/validators/exercise.validators.ts
Normal file
14
backend/src/validators/exercise.validators.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const createExerciseSchema = z.object({
|
||||
name: z.string().min(2).max(50),
|
||||
description: z.string().max(300).optional(),
|
||||
difficulty: z.enum(["BEGINNER", "INTERMEDIATE", "ADVANCED"]).default("BEGINNER"),
|
||||
muscleGroups: z.array(z.string()).min(1),
|
||||
modelPath: z.string().optional(),
|
||||
});
|
||||
|
||||
export const updateExerciseSchema = createExerciseSchema.partial();
|
||||
|
||||
export type CreateExerciseInput = z.infer<typeof createExerciseSchema>;
|
||||
export type UpdateExerciseInput = z.infer<typeof updateExerciseSchema>;
|
||||
25
backend/src/validators/program.validators.ts
Normal file
25
backend/src/validators/program.validators.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const programExerciseSchema = z.object({
|
||||
exerciseId: z.string().uuid(),
|
||||
sets: z.number().int().min(1),
|
||||
reps: z.number().int().min(1).optional(),
|
||||
durationSec: z.number().int().min(1).optional(),
|
||||
order: z.number().int().min(0).default(0),
|
||||
});
|
||||
|
||||
export const createProgramSchema = z.object({
|
||||
name: z.string().min(2).max(50),
|
||||
description: z.string().max(300).optional(),
|
||||
isPublic: z.boolean().default(false),
|
||||
exercises: z.array(programExerciseSchema).min(1),
|
||||
});
|
||||
|
||||
export const updateProgramSchema = z.object({
|
||||
name: z.string().min(2).max(50).optional(),
|
||||
description: z.string().max(300).optional(),
|
||||
isPublic: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type CreateProgramInput = z.infer<typeof createProgramSchema>;
|
||||
export type UpdateProgramInput = z.infer<typeof updateProgramSchema>;
|
||||
Reference in New Issue
Block a user