feat: phase 1 — auth + user endpoints, Prisma v7 adapter, DB init

This commit is contained in:
2026-03-26 03:41:39 +00:00
parent 2d5030c521
commit 48446b483c
13 changed files with 675 additions and 16 deletions

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

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

View File

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

View File

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

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

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

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

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