feat: phase 1 — auth + user endpoints, Prisma v7 adapter, DB init
This commit is contained in:
93
backend/src/controllers/auth.controller.ts
Normal file
93
backend/src/controllers/auth.controller.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import type { Response } from "express";
|
||||
import argon2 from "argon2";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { prisma } from "../index";
|
||||
import type { AppRequest } from "../types/context";
|
||||
import { loginSchema, registerSchema } from "../validators/auth.validators";
|
||||
|
||||
const COOKIE_OPTIONS = {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax" as const,
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 jours
|
||||
};
|
||||
|
||||
export async function register(req: AppRequest, res: Response): Promise<void> {
|
||||
const parsed = registerSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||
return;
|
||||
}
|
||||
|
||||
const { username, email, password } = parsed.data;
|
||||
|
||||
const existing = await prisma.user.findFirst({
|
||||
where: { OR: [{ email }, { username }] },
|
||||
});
|
||||
if (existing) {
|
||||
res.status(409).json({ message: "Email ou username déjà utilisé." });
|
||||
return;
|
||||
}
|
||||
|
||||
const hashed = await argon2.hash(password);
|
||||
const user = await prisma.user.create({
|
||||
data: { username, email, password: hashed },
|
||||
});
|
||||
|
||||
const token = jwt.sign(
|
||||
{ id: user.id, email: user.email, role: user.role },
|
||||
process.env.JWT_SECRET as string,
|
||||
{ expiresIn: "7d" }
|
||||
);
|
||||
|
||||
res.cookie("token", token, COOKIE_OPTIONS);
|
||||
res.status(201).json({ id: user.id, username: user.username, email: user.email, role: user.role });
|
||||
}
|
||||
|
||||
export async function login(req: AppRequest, res: Response): Promise<void> {
|
||||
const parsed = loginSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||
return;
|
||||
}
|
||||
|
||||
const { email, password } = parsed.data;
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { email } });
|
||||
if (!user) {
|
||||
res.status(401).json({ message: "Identifiants invalides." });
|
||||
return;
|
||||
}
|
||||
|
||||
const valid = await argon2.verify(user.password, password);
|
||||
if (!valid) {
|
||||
res.status(401).json({ message: "Identifiants invalides." });
|
||||
return;
|
||||
}
|
||||
|
||||
const token = jwt.sign(
|
||||
{ id: user.id, email: user.email, role: user.role },
|
||||
process.env.JWT_SECRET as string,
|
||||
{ expiresIn: "7d" }
|
||||
);
|
||||
|
||||
res.cookie("token", token, COOKIE_OPTIONS);
|
||||
res.json({ id: user.id, username: user.username, email: user.email, role: user.role });
|
||||
}
|
||||
|
||||
export function logout(_req: AppRequest, res: Response): void {
|
||||
res.clearCookie("token");
|
||||
res.json({ message: "Déconnecté." });
|
||||
}
|
||||
|
||||
export async function me(req: AppRequest, res: Response): Promise<void> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.user!.id },
|
||||
select: { id: true, username: true, email: true, role: true, avatar: true, bio: true, createdAt: true },
|
||||
});
|
||||
if (!user) {
|
||||
res.status(404).json({ message: "Utilisateur introuvable." });
|
||||
return;
|
||||
}
|
||||
res.json(user);
|
||||
}
|
||||
41
backend/src/controllers/user.controller.ts
Normal file
41
backend/src/controllers/user.controller.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { Response } from "express";
|
||||
import { prisma } from "../index";
|
||||
import type { AppRequest } from "../types/context";
|
||||
import { updateUserSchema } from "../validators/user.validators";
|
||||
|
||||
export async function getMe(req: AppRequest, res: Response): Promise<void> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.user!.id },
|
||||
select: { id: true, username: true, email: true, role: true, avatar: true, bio: true, createdAt: true },
|
||||
});
|
||||
if (!user) {
|
||||
res.status(404).json({ message: "Utilisateur introuvable." });
|
||||
return;
|
||||
}
|
||||
res.json(user);
|
||||
}
|
||||
|
||||
export async function updateMe(req: AppRequest, res: Response): Promise<void> {
|
||||
const parsed = updateUserSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed.data.username) {
|
||||
const taken = await prisma.user.findFirst({
|
||||
where: { username: parsed.data.username, NOT: { id: req.user!.id } },
|
||||
});
|
||||
if (taken) {
|
||||
res.status(409).json({ message: "Username déjà utilisé." });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: { id: req.user!.id },
|
||||
data: parsed.data,
|
||||
select: { id: true, username: true, email: true, role: true, avatar: true, bio: true },
|
||||
});
|
||||
res.json(user);
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
import "dotenv/config";
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import cookieParser from "cookie-parser";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import authRoutes from "./routes/auth.routes";
|
||||
import userRoutes from "./routes/user.routes";
|
||||
|
||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||
const app = express();
|
||||
const prisma = new PrismaClient();
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
const PORT = process.env.PORT ?? 4000;
|
||||
const CLIENT_URL = process.env.CLIENT_URL ?? "http://localhost:5173";
|
||||
@@ -15,23 +20,18 @@ if (!process.env.JWT_SECRET) {
|
||||
|
||||
// ─── Middlewares ──────────────────────────────────────────────────────────────
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: CLIENT_URL,
|
||||
credentials: true,
|
||||
})
|
||||
);
|
||||
app.use(cors({ origin: CLIENT_URL, credentials: true }));
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
|
||||
// ─── Static uploads ──────────────────────────────────────────────────────────
|
||||
// ─── Static uploads ───────────────────────────────────────────────────────────
|
||||
|
||||
app.use("/uploads", express.static("uploads"));
|
||||
|
||||
// ─── Routes ──────────────────────────────────────────────────────────────────
|
||||
// ─── Routes ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// app.use("/api/auth", authRoutes);
|
||||
// app.use("/api/users", userRoutes);
|
||||
app.use("/api/auth", authRoutes);
|
||||
app.use("/api/users", userRoutes);
|
||||
// app.use("/api/exercises", exerciseRoutes);
|
||||
// app.use("/api/programs", programRoutes);
|
||||
// app.use("/api/groups", groupRoutes);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Response, NextFunction } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import type { AppRequest, AuthPayload } from "../types/context.js";
|
||||
import type { AppRequest, AuthPayload } from "../types/context";
|
||||
|
||||
export function requireAuth(req: AppRequest, res: Response, next: NextFunction): void {
|
||||
const token = req.cookies?.token as string | undefined;
|
||||
|
||||
12
backend/src/routes/auth.routes.ts
Normal file
12
backend/src/routes/auth.routes.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Router } from "express";
|
||||
import { login, logout, me, register } from "../controllers/auth.controller";
|
||||
import { requireAuth } from "../middlewares/auth";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/register", register);
|
||||
router.post("/login", login);
|
||||
router.post("/logout", logout);
|
||||
router.get("/me", requireAuth, me);
|
||||
|
||||
export default router;
|
||||
10
backend/src/routes/user.routes.ts
Normal file
10
backend/src/routes/user.routes.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Router } from "express";
|
||||
import { getMe, updateMe } from "../controllers/user.controller";
|
||||
import { requireAuth } from "../middlewares/auth";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/me", requireAuth, getMe);
|
||||
router.patch("/me", requireAuth, updateMe);
|
||||
|
||||
export default router;
|
||||
15
backend/src/validators/auth.validators.ts
Normal file
15
backend/src/validators/auth.validators.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const registerSchema = z.object({
|
||||
username: z.string().min(3).max(20),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
});
|
||||
|
||||
export const loginSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
export type RegisterInput = z.infer<typeof registerSchema>;
|
||||
export type LoginInput = z.infer<typeof loginSchema>;
|
||||
9
backend/src/validators/user.validators.ts
Normal file
9
backend/src/validators/user.validators.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const updateUserSchema = z.object({
|
||||
username: z.string().min(3).max(20).optional(),
|
||||
bio: z.string().max(300).optional(),
|
||||
avatar: z.string().url().optional(),
|
||||
});
|
||||
|
||||
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
|
||||
Reference in New Issue
Block a user