Checkpoint: Migration OAuth Manus vers authentification locale (80% complété) - Pages login/register créées, backend JWT implémenté, guides de déploiement ajoutés. Reste à corriger les doublons dans db.ts et finaliser AdminUsers.

This commit is contained in:
Manus Sandbox
2025-11-19 04:39:07 -05:00
parent 9d67724e83
commit 28727171ae
17 changed files with 2168 additions and 87 deletions

43
server/_core/auth.ts Normal file
View File

@@ -0,0 +1,43 @@
import bcrypt from "bcrypt";
import { sign, verify } from "jsonwebtoken";
import { ENV } from "./env";
const SALT_ROUNDS = 10;
/**
* Hashe un mot de passe avec bcrypt
*/
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
/**
* Vérifie un mot de passe contre son hash
*/
export async function verifyPassword(
password: string,
hash: string
): Promise<boolean> {
return bcrypt.compare(password, hash);
}
/**
* Génère un token JWT pour un utilisateur
*/
export function generateToken(userId: number): string {
return sign({ userId }, ENV.jwtSecret, {
expiresIn: "30d",
});
}
/**
* Vérifie et décode un token JWT
*/
export function verifyToken(token: string): { userId: number } | null {
try {
const decoded = verify(token, ENV.jwtSecret) as { userId: number };
return decoded;
} catch {
return null;
}
}

View File

@@ -1,6 +1,8 @@
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
import type { User } from "../../drizzle/schema";
import { sdk } from "./sdk";
import { verifyToken } from "./auth";
import { getUserById } from "../db";
import { COOKIE_NAME } from "@shared/const";
export type TrpcContext = {
req: CreateExpressContextOptions["req"];
@@ -14,7 +16,18 @@ export async function createContext(
let user: User | null = null;
try {
user = await sdk.authenticateRequest(opts.req);
// Récupérer le token JWT depuis le cookie
const token = opts.req.cookies[COOKIE_NAME];
if (token) {
// Vérifier et décoder le token
const decoded = verifyToken(token);
if (decoded) {
// Récupérer l'utilisateur depuis la base de données
user = await getUserById(decoded.userId) || null;
}
}
} catch (error) {
// Authentication is optional for public procedures.
user = null;

View File

@@ -42,77 +42,72 @@ export async function getDb() {
return _db;
}
export async function upsertUser(user: InsertUser): Promise<void> {
if (!user.openId) {
throw new Error("User openId is required for upsert");
/**
* Crée un nouvel utilisateur avec email/mot de passe
*/
export async function createUser(user: InsertUser): Promise<number> {
if (!user.email) {
throw new Error("User email is required");
}
if (!user.password) {
throw new Error("User password is required");
}
const db = await getDb();
if (!db) {
console.warn("[Database] Cannot upsert user: database not available");
return;
throw new Error("Database not available");
}
try {
const values: InsertUser = {
openId: user.openId,
};
const updateSet: Record<string, unknown> = {};
const textFields = ["name", "email", "loginMethod"] as const;
type TextField = (typeof textFields)[number];
const assignNullable = (field: TextField) => {
const value = user[field];
if (value === undefined) return;
const normalized = value ?? null;
values[field] = normalized;
updateSet[field] = normalized;
};
textFields.forEach(assignNullable);
if (user.lastSignedIn !== undefined) {
values.lastSignedIn = user.lastSignedIn;
updateSet.lastSignedIn = user.lastSignedIn;
}
if (user.role !== undefined) {
values.role = user.role;
updateSet.role = user.role;
} else if (user.openId === ENV.ownerOpenId) {
values.role = 'admin';
updateSet.role = 'admin';
}
if (!values.lastSignedIn) {
values.lastSignedIn = new Date();
}
if (Object.keys(updateSet).length === 0) {
updateSet.lastSignedIn = new Date();
}
await db.insert(users).values(values).onDuplicateKeyUpdate({
set: updateSet,
});
const result = await db.insert(users).values(user);
return Number(result[0].insertId);
} catch (error) {
console.error("[Database] Failed to upsert user:", error);
console.error("[Database] Failed to create user:", error);
throw error;
}
}
export async function getUserByOpenId(openId: string) {
/**
* Récupère un utilisateur par son email
*/
export async function getUserByEmail(email: string) {
const db = await getDb();
if (!db) {
console.warn("[Database] Cannot get user: database not available");
return undefined;
}
const result = await db.select().from(users).where(eq(users.openId, openId)).limit(1);
const result = await db.select().from(users).where(eq(users.email, email)).limit(1);
return result.length > 0 ? result[0] : undefined;
}
/**
* Récupère un utilisateur par son ID
*/
export async function getUserById(id: number) {
const db = await getDb();
if (!db) {
console.warn("[Database] Cannot get user: database not available");
return undefined;
}
const result = await db.select().from(users).where(eq(users.id, id)).limit(1);
return result.length > 0 ? result[0] : undefined;
}
/**
* Met à jour le dernier login d'un utilisateur
*/
export async function updateUserLastSignedIn(userId: number): Promise<void> {
const db = await getDb();
if (!db) {
console.warn("[Database] Cannot update user: database not available");
return;
}
await db.update(users).set({ lastSignedIn: new Date() }).where(eq(users.id, userId));
}
// ==================== FORMATIONS ====================
export async function createFormation(data: InsertFormation) {
@@ -383,14 +378,6 @@ export async function deleteInscription(id: number) {
// ==================== GESTION DES UTILISATEURS ====================
export async function createUser(data: InsertUser) {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(users).values(data);
return result;
}
export async function getAllUsers() {
const db = await getDb();
if (!db) return [];
@@ -398,14 +385,6 @@ export async function getAllUsers() {
return await db.select().from(users);
}
export async function getUserById(id: number) {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(users).where(eq(users.id, id)).limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function updateUser(id: number, data: Partial<InsertUser>) {
const db = await getDb();
if (!db) throw new Error("Database not available");

View File

@@ -21,6 +21,83 @@ export const appRouter = router({
system: systemRouter,
auth: router({
me: publicProcedure.query(opts => opts.ctx.user),
register: publicProcedure
.input(z.object({
email: z.string().email(),
password: z.string().min(6),
name: z.string().optional(),
}))
.mutation(async ({ input, ctx }) => {
const { hashPassword, generateToken } = await import("./_core/auth");
// Vérifier si l'utilisateur existe déjà
const existingUser = await db.getUserByEmail(input.email);
if (existingUser) {
throw new TRPCError({ code: 'CONFLICT', message: 'Un compte existe déjà avec cet email' });
}
// Hasher le mot de passe
const hashedPassword = await hashPassword(input.password);
// Créer l'utilisateur
const userId = await db.createUser({
email: input.email,
password: hashedPassword,
name: input.name,
emailVerified: false,
role: 'user',
isActive: true,
});
// Générer un token JWT
const token = generateToken(userId);
// Définir le cookie de session
const cookieOptions = getSessionCookieOptions(ctx.req);
ctx.res.cookie(COOKIE_NAME, token, cookieOptions);
return { success: true, userId };
}),
login: publicProcedure
.input(z.object({
email: z.string().email(),
password: z.string(),
}))
.mutation(async ({ input, ctx }) => {
const { verifyPassword, generateToken } = await import("./_core/auth");
// Récupérer l'utilisateur
const user = await db.getUserByEmail(input.email);
if (!user) {
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Email ou mot de passe incorrect' });
}
// Vérifier le compte actif
if (!user.isActive) {
throw new TRPCError({ code: 'FORBIDDEN', message: 'Votre compte a été désactivé' });
}
// Vérifier le mot de passe
const isValid = await verifyPassword(input.password, user.password);
if (!isValid) {
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Email ou mot de passe incorrect' });
}
// Mettre à jour le dernier login
await db.updateUserLastSignedIn(user.id);
// Générer un token JWT
const token = generateToken(user.id);
// Définir le cookie de session
const cookieOptions = getSessionCookieOptions(ctx.req);
ctx.res.cookie(COOKIE_NAME, token, cookieOptions);
return { success: true, user: { id: user.id, email: user.email, name: user.name, role: user.role } };
}),
logout: publicProcedure.mutation(({ ctx }) => {
const cookieOptions = getSessionCookieOptions(ctx.req);
ctx.res.clearCookie(COOKIE_NAME, { ...cookieOptions, maxAge: -1 });