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:
43
server/_core/auth.ts
Normal file
43
server/_core/auth.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
111
server/db.ts
111
server/db.ts
@@ -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");
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user