- Authentification locale avec email/mot de passe - OAuth Manus pour les utilisateurs de la plateforme Le contexte tRPC gère automatiquement les deux types de tokens (JWT local et JWT OAuth).
718 lines
19 KiB
TypeScript
718 lines
19 KiB
TypeScript
import { eq, and, sql } from "drizzle-orm";
|
|
import { drizzle } from "drizzle-orm/mysql2";
|
|
import {
|
|
InsertUser,
|
|
users,
|
|
formations,
|
|
apprenants,
|
|
sequences,
|
|
datesFormation,
|
|
inscriptions,
|
|
passwordResetTokens,
|
|
InsertPasswordResetToken,
|
|
emailTemplates,
|
|
emailConfig,
|
|
InsertEmailConfig,
|
|
InsertSequence,
|
|
InsertDateFormation,
|
|
InsertInscription,
|
|
InsertFormation,
|
|
InsertApprenant,
|
|
Sequence,
|
|
DateFormation,
|
|
Apprenant,
|
|
Formation,
|
|
EmailTemplate,
|
|
InsertEmailTemplate
|
|
} from "../drizzle/schema";
|
|
import { ENV } from './_core/env';
|
|
|
|
let _db: ReturnType<typeof drizzle> | null = null;
|
|
|
|
// Lazily create the drizzle instance so local tooling can run without a DB.
|
|
export async function getDb() {
|
|
if (!_db && process.env.DATABASE_URL) {
|
|
try {
|
|
_db = drizzle(process.env.DATABASE_URL);
|
|
} catch (error) {
|
|
console.warn("[Database] Failed to connect:", error);
|
|
_db = null;
|
|
}
|
|
}
|
|
return _db;
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
throw new Error("Database not available");
|
|
}
|
|
|
|
try {
|
|
const result = await db.insert(users).values(user);
|
|
return Number(result[0].insertId);
|
|
} catch (error) {
|
|
console.error("[Database] Failed to create user:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.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;
|
|
}
|
|
|
|
/**
|
|
* Récupère un utilisateur par son openId (OAuth)
|
|
*/
|
|
export async function getUserByOpenId(openId: 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);
|
|
return result.length > 0 ? result[0] : undefined;
|
|
}
|
|
|
|
/**
|
|
* Crée ou met à jour un utilisateur OAuth
|
|
*/
|
|
export async function upsertUser(user: InsertUser): Promise<void> {
|
|
if (!user.openId && !user.email) {
|
|
throw new Error("User openId or email is required for upsert");
|
|
}
|
|
|
|
const db = await getDb();
|
|
if (!db) {
|
|
console.warn("[Database] Cannot upsert user: database not available");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const values: InsertUser = {
|
|
openId: user.openId,
|
|
email: user.email || '',
|
|
};
|
|
const updateSet: Record<string, unknown> = {};
|
|
|
|
const textFields = ["name", "email"] 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();
|
|
}
|
|
|
|
// Utiliser openId comme clé unique pour OAuth
|
|
if (user.openId) {
|
|
await db.insert(users).values(values).onDuplicateKeyUpdate({
|
|
set: updateSet,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error("[Database] Failed to upsert user:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
const result = await db.insert(formations).values(data);
|
|
return result;
|
|
}
|
|
|
|
export async function getFormations() {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
|
|
return await db.select().from(formations);
|
|
}
|
|
|
|
export async function getFormationById(id: number) {
|
|
const db = await getDb();
|
|
if (!db) return undefined;
|
|
|
|
const result = await db.select().from(formations).where(eq(formations.id, id)).limit(1);
|
|
return result.length > 0 ? result[0] : undefined;
|
|
}
|
|
|
|
export async function getFormationByLien(lien: string) {
|
|
const db = await getDb();
|
|
if (!db) return undefined;
|
|
|
|
const result = await db.select().from(formations).where(eq(formations.lienUnique, lien)).limit(1);
|
|
return result.length > 0 ? result[0] : undefined;
|
|
}
|
|
|
|
export async function updateFormation(id: number, data: Partial<InsertFormation>) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
await db.update(formations).set(data).where(eq(formations.id, id));
|
|
}
|
|
|
|
export async function deleteFormation(id: number) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
await db.delete(formations).where(eq(formations.id, id));
|
|
}
|
|
|
|
// ==================== SÉQUENCES ====================
|
|
|
|
export async function createSequence(data: InsertSequence) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
const result = await db.insert(sequences).values(data);
|
|
return result;
|
|
}
|
|
|
|
export async function getSequences() {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
|
|
return await db.select().from(sequences);
|
|
}
|
|
|
|
export async function getSequenceById(id: number): Promise<Sequence | undefined> {
|
|
const db = await getDb();
|
|
if (!db) return undefined;
|
|
|
|
const result = await db.select().from(sequences).where(eq(sequences.id, id)).limit(1);
|
|
return result.length > 0 ? result[0] : undefined;
|
|
}
|
|
|
|
export async function getSequencesByFormation(formationId: number) {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
|
|
return await db.select().from(sequences).where(eq(sequences.formationId, formationId));
|
|
}
|
|
|
|
export async function updateSequence(id: number, data: Partial<InsertSequence>) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
await db.update(sequences).set(data).where(eq(sequences.id, id));
|
|
}
|
|
|
|
export async function deleteSequence(id: number) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
await db.delete(sequences).where(eq(sequences.id, id));
|
|
}
|
|
|
|
// ==================== DATES DE FORMATION ====================
|
|
|
|
export async function createDateFormation(data: InsertDateFormation) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
const result = await db.insert(datesFormation).values(data);
|
|
return result;
|
|
}
|
|
|
|
export async function getDatesBySequence(sequenceId: number): Promise<DateFormation[]> {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
|
|
const results = await db.select()
|
|
.from(datesFormation)
|
|
.where(eq(datesFormation.sequenceId, sequenceId))
|
|
.orderBy(datesFormation.ordre);
|
|
|
|
return results;
|
|
}
|
|
|
|
export async function deleteDatesBySequence(sequenceId: number) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
await db.delete(datesFormation).where(eq(datesFormation.sequenceId, sequenceId));
|
|
}
|
|
|
|
// ==================== APPRENANTS ====================
|
|
|
|
export async function createApprenant(data: InsertApprenant) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
const result = await db.insert(apprenants).values(data);
|
|
return result;
|
|
}
|
|
|
|
export async function getApprenants() {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
|
|
return await db.select().from(apprenants);
|
|
}
|
|
|
|
export async function getApprenantById(id: number): Promise<Apprenant | undefined> {
|
|
const db = await getDb();
|
|
if (!db) return undefined;
|
|
|
|
const result = await db.select().from(apprenants).where(eq(apprenants.id, id)).limit(1);
|
|
return result.length > 0 ? result[0] : undefined;
|
|
}
|
|
|
|
export async function getApprenantByEmail(email: string) {
|
|
const db = await getDb();
|
|
if (!db) return undefined;
|
|
|
|
const result = await db.select().from(apprenants).where(eq(apprenants.email, email)).limit(1);
|
|
return result.length > 0 ? result[0] : undefined;
|
|
}
|
|
|
|
export async function updateApprenant(id: number, data: Partial<InsertApprenant>) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
await db.update(apprenants).set(data).where(eq(apprenants.id, id));
|
|
}
|
|
|
|
export async function deleteApprenant(id: number) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
await db.delete(apprenants).where(eq(apprenants.id, id));
|
|
}
|
|
|
|
// ==================== INSCRIPTIONS ====================
|
|
|
|
export async function createInscription(data: InsertInscription) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
const result = await db.insert(inscriptions).values(data);
|
|
return result;
|
|
}
|
|
|
|
export async function getAllInscriptions() {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
|
|
const results = await db.select({
|
|
inscription: inscriptions,
|
|
apprenant: apprenants,
|
|
})
|
|
.from(inscriptions)
|
|
.leftJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id));
|
|
|
|
return results;
|
|
}
|
|
|
|
export async function getInscriptionsBySequence(sequenceId: number) {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
|
|
const results = await db.select({
|
|
inscription: inscriptions,
|
|
apprenant: apprenants,
|
|
})
|
|
.from(inscriptions)
|
|
.leftJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id))
|
|
.where(eq(inscriptions.sequenceId, sequenceId));
|
|
|
|
return results;
|
|
}
|
|
|
|
export async function getInscriptionsByApprenant(apprenantId: number) {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
|
|
const results = await db.select({
|
|
inscription: inscriptions,
|
|
sequence: sequences,
|
|
})
|
|
.from(inscriptions)
|
|
.leftJoin(sequences, eq(inscriptions.sequenceId, sequences.id))
|
|
.where(eq(inscriptions.apprenantId, apprenantId));
|
|
|
|
return results;
|
|
}
|
|
|
|
export async function checkExistingInscription(apprenantId: number, sequenceId: number) {
|
|
const db = await getDb();
|
|
if (!db) return undefined;
|
|
|
|
const result = await db.select()
|
|
.from(inscriptions)
|
|
.where(and(
|
|
eq(inscriptions.apprenantId, apprenantId),
|
|
eq(inscriptions.sequenceId, sequenceId)
|
|
))
|
|
.limit(1);
|
|
|
|
return result.length > 0 ? result[0] : undefined;
|
|
}
|
|
|
|
export async function countInscriptionsBySequence(sequenceId: number, statut?: string) {
|
|
const db = await getDb();
|
|
if (!db) return 0;
|
|
|
|
const conditions = [eq(inscriptions.sequenceId, sequenceId)];
|
|
if (statut) {
|
|
conditions.push(eq(inscriptions.statut, statut as any));
|
|
}
|
|
|
|
const result = await db.select({ count: sql<number>`count(*)` })
|
|
.from(inscriptions)
|
|
.where(and(...conditions));
|
|
|
|
return result[0]?.count || 0;
|
|
}
|
|
|
|
export async function updateInscription(id: number, data: Partial<InsertInscription>) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
await db.update(inscriptions).set(data).where(eq(inscriptions.id, id));
|
|
}
|
|
|
|
export async function deleteInscription(id: number) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
await db.delete(inscriptions).where(eq(inscriptions.id, id));
|
|
}
|
|
|
|
// ==================== GESTION DES UTILISATEURS ====================
|
|
|
|
export async function getAllUsers() {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
|
|
return await db.select().from(users);
|
|
}
|
|
|
|
export async function updateUser(id: number, data: Partial<InsertUser>) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
await db.update(users).set(data).where(eq(users.id, id));
|
|
}
|
|
|
|
export async function toggleUserStatus(id: number, isActive: boolean) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
await db.update(users).set({ isActive }).where(eq(users.id, id));
|
|
}
|
|
|
|
export async function deleteUser(id: number) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
await db.delete(users).where(eq(users.id, id));
|
|
}
|
|
|
|
|
|
// ==================== GESTION DES TOKENS DE RÉINITIALISATION ====================
|
|
|
|
export async function createPasswordResetToken(userId: number, token: string, expiresAt: Date) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
const result = await db.insert(passwordResetTokens).values({
|
|
userId,
|
|
token,
|
|
expiresAt,
|
|
used: false,
|
|
});
|
|
return result;
|
|
}
|
|
|
|
export async function getPasswordResetToken(token: string) {
|
|
const db = await getDb();
|
|
if (!db) return undefined;
|
|
|
|
const result = await db.select()
|
|
.from(passwordResetTokens)
|
|
.where(eq(passwordResetTokens.token, token))
|
|
.limit(1);
|
|
|
|
return result.length > 0 ? result[0] : undefined;
|
|
}
|
|
|
|
export async function markTokenAsUsed(tokenId: number) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
await db.update(passwordResetTokens)
|
|
.set({ used: true })
|
|
.where(eq(passwordResetTokens.id, tokenId));
|
|
}
|
|
|
|
export async function deleteExpiredTokens() {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
const now = new Date();
|
|
await db.delete(passwordResetTokens)
|
|
.where(sql`${passwordResetTokens.expiresAt} < ${now}`);
|
|
}
|
|
|
|
// ============================================
|
|
// Email Templates Management
|
|
// ============================================
|
|
|
|
/**
|
|
* Récupère tous les templates d'emails
|
|
*/
|
|
export async function getAllEmailTemplates() {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
|
|
return await db.select().from(emailTemplates);
|
|
}
|
|
|
|
/**
|
|
* Récupère un template par son type
|
|
*/
|
|
export async function getEmailTemplateByType(type: string) {
|
|
const db = await getDb();
|
|
if (!db) return undefined;
|
|
|
|
const results = await db.select().from(emailTemplates).where(eq(emailTemplates.type, type)).limit(1);
|
|
return results.length > 0 ? results[0] : undefined;
|
|
}
|
|
|
|
/**
|
|
* Crée ou met à jour un template d'email
|
|
*/
|
|
export async function upsertEmailTemplate(template: InsertEmailTemplate) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
const existing = await getEmailTemplateByType(template.type);
|
|
|
|
if (existing) {
|
|
// Mise à jour
|
|
await db.update(emailTemplates)
|
|
.set({
|
|
name: template.name,
|
|
logoUrl: template.logoUrl,
|
|
primaryColor: template.primaryColor,
|
|
headerBgColor: template.headerBgColor,
|
|
headerTextColor: template.headerTextColor,
|
|
headerTitle: template.headerTitle,
|
|
footerText: template.footerText,
|
|
active: template.active,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(emailTemplates.type, template.type));
|
|
|
|
return await getEmailTemplateByType(template.type);
|
|
} else {
|
|
// Création
|
|
await db.insert(emailTemplates).values(template);
|
|
return await getEmailTemplateByType(template.type);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Supprime un template d'email
|
|
*/
|
|
export async function deleteEmailTemplate(type: string) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
await db.delete(emailTemplates).where(eq(emailTemplates.type, type));
|
|
}
|
|
|
|
/**
|
|
* Initialise les templates par défaut si la table est vide
|
|
*/
|
|
export async function initializeDefaultEmailTemplates() {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
|
|
const existing = await getAllEmailTemplates();
|
|
if (existing.length > 0) return; // Déjà initialisé
|
|
|
|
const defaultTemplates: InsertEmailTemplate[] = [
|
|
{
|
|
type: "inscription",
|
|
name: "Confirmation d'inscription",
|
|
logoUrl: null,
|
|
primaryColor: "#2563eb",
|
|
headerBgColor: "#2563eb",
|
|
headerTextColor: "#ffffff",
|
|
headerTitle: "Formation Manager Itinova",
|
|
footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
|
active: true,
|
|
},
|
|
{
|
|
type: "teaser",
|
|
name: "Email teaser",
|
|
logoUrl: null,
|
|
primaryColor: "#2563eb",
|
|
headerBgColor: "#2563eb",
|
|
headerTextColor: "#ffffff",
|
|
headerTitle: "Formation Manager Itinova",
|
|
footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
|
active: true,
|
|
},
|
|
{
|
|
type: "rappel",
|
|
name: "Rappel J-7",
|
|
logoUrl: null,
|
|
primaryColor: "#2563eb",
|
|
headerBgColor: "#2563eb",
|
|
headerTextColor: "#ffffff",
|
|
headerTitle: "Formation Manager Itinova",
|
|
footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
|
active: true,
|
|
},
|
|
{
|
|
type: "reset_password",
|
|
name: "Réinitialisation de mot de passe",
|
|
logoUrl: null,
|
|
primaryColor: "#2563eb",
|
|
headerBgColor: "#2563eb",
|
|
headerTextColor: "#ffffff",
|
|
headerTitle: "Formation Manager Itinova",
|
|
footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
|
active: true,
|
|
},
|
|
];
|
|
|
|
for (const template of defaultTemplates) {
|
|
await db.insert(emailTemplates).values(template);
|
|
}
|
|
|
|
console.log("[DB] Templates d'emails par défaut initialisés");
|
|
}
|
|
|
|
|
|
// ==================== Email Config ====================
|
|
|
|
/**
|
|
* Récupère la configuration email active
|
|
*/
|
|
export async function getActiveEmailConfig() {
|
|
const db = await getDb();
|
|
if (!db) return undefined;
|
|
|
|
const result = await db
|
|
.select()
|
|
.from(emailConfig)
|
|
.where(eq(emailConfig.active, true))
|
|
.limit(1);
|
|
|
|
return result.length > 0 ? result[0] : undefined;
|
|
}
|
|
|
|
/**
|
|
* Crée ou met à jour la configuration email
|
|
*/
|
|
export async function upsertEmailConfig(data: InsertEmailConfig) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
|
|
// Désactiver toutes les configurations existantes
|
|
await db.update(emailConfig).set({ active: false });
|
|
|
|
// Créer la nouvelle configuration
|
|
await db.insert(emailConfig).values({
|
|
...data,
|
|
active: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Met à jour la configuration email
|
|
*/
|
|
export async function updateEmailConfig(id: number, data: Partial<InsertEmailConfig>) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
|
|
await db
|
|
.update(emailConfig)
|
|
.set({
|
|
...data,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(emailConfig.id, id));
|
|
}
|