retrotoon-studio/server/db.ts
Ubuntu 20a643c4ce fix: audit complet et pipeline fonctionnel RetroToon Studio
Corrections critiques:
- Fix titre HTML {{project_title}} -> %VITE_APP_TITLE%
- Suppression vitePluginManusRuntime (360KB -> 4KB index.html)
- Upload vidéo: multer au lieu du parsing binary maison (anti-corruption)
- Extraction audio ffmpeg + sauvegarde sourceAudioUrl en DB
- Page /login dédiée + correction redirect auth
- Test moteurs IA: vrai HEAD request avec latence
- Suppression spam logs [Auth] Missing session cookie
- Fix fuite passwordHash dans auth.me
- Cookie sameSite: none -> lax (CSRF)

Sécurité:
- Endpoints admin protégés par adminProcedure (role=admin requis)
- Sidebar admin masquée pour non-admins
- AdminPanel: page accès refusé pour non-admins
- Bootstrap admin optimisé (skip rehash si identique)

Fonctionnalités:
- Export vidéo MP4 réel via ffmpeg local (H.264 + AAC audio)
- Download parallèle par batch de 20 (export 10x plus rapide)
- Détection de scènes réelle via ffmpeg scene detect
- Analyse arrière-plans via Gemini Vision (remplace Math.random)
- Gemini: conservation du role system + support image_url
- Suppression thinking.budget_tokens:128 (LLM config)
- Thumbnails de frames dans la timeline
- Toast export avec bouton télécharger
- Endpoint extraction audio à la demande

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 01:37:08 +00:00

271 lines
9.3 KiB
TypeScript

import { eq, and, desc, asc } from "drizzle-orm";
import { drizzle } from "drizzle-orm/mysql2";
import {
InsertUser, users,
projects, InsertProject, Project,
sequences, InsertSequence,
frames, InsertFrame,
layers, InsertLayer,
characters, InsertCharacter,
aiEngines, InsertAiEngine,
generationJobs, InsertGenerationJob,
assistantMessages, InsertAssistantMessage,
} from "../drizzle/schema";
import { ENV } from './_core/env';
let _db: ReturnType<typeof drizzle> | null = null;
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;
}
// ============ USERS ============
export async function upsertUser(user: InsertUser): Promise<void> {
if (!user.openId) throw new Error("User openId is required for upsert");
const db = await getDb();
if (!db) return;
const values: InsertUser = { openId: user.openId };
const updateSet: Record<string, unknown> = {};
const textFields = ["name", "email", "passwordHash", "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 });
}
export async function getUserByOpenId(openId: string) {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(users).where(eq(users.openId, openId)).limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function getUserByEmail(email: string) {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(users).where(eq(users.email, email)).limit(1);
return result.length > 0 ? result[0] : undefined;
}
// ============ PROJECTS ============
export async function createProject(data: InsertProject) {
const db = await getDb();
if (!db) throw new Error("DB not available");
const result = await db.insert(projects).values(data);
return { id: result[0].insertId };
}
export async function getProject(id: number) {
const db = await getDb();
if (!db) return null;
const result = await db.select().from(projects).where(eq(projects.id, id)).limit(1);
return result[0] ?? null;
}
export async function listProjects(userId: number) {
const db = await getDb();
if (!db) return [];
return db.select().from(projects).where(eq(projects.userId, userId)).orderBy(desc(projects.createdAt));
}
export async function updateProject(id: number, data: Partial<InsertProject>) {
const db = await getDb();
if (!db) return;
await db.update(projects).set(data).where(eq(projects.id, id));
}
export async function deleteProject(id: number) {
const db = await getDb();
if (!db) return;
// Delete related data first (cascade)
await db.delete(assistantMessages).where(eq(assistantMessages.projectId, id));
await db.delete(generationJobs).where(eq(generationJobs.projectId, id));
await db.delete(layers).where(eq(layers.projectId, id));
await db.delete(characters).where(eq(characters.projectId, id));
await db.delete(sequences).where(eq(sequences.projectId, id));
await db.delete(frames).where(eq(frames.projectId, id));
await db.delete(projects).where(eq(projects.id, id));
}
// ============ SEQUENCES ============
export async function createSequence(data: InsertSequence) {
const db = await getDb();
if (!db) throw new Error("DB not available");
const result = await db.insert(sequences).values(data);
return { id: result[0].insertId };
}
export async function listSequences(projectId: number) {
const db = await getDb();
if (!db) return [];
return db.select().from(sequences).where(eq(sequences.projectId, projectId)).orderBy(asc(sequences.startFrame));
}
export async function updateSequence(id: number, data: Partial<InsertSequence>) {
const db = await getDb();
if (!db) return;
await db.update(sequences).set(data).where(eq(sequences.id, id));
}
// ============ FRAMES ============
export async function createFrames(data: InsertFrame[]) {
const db = await getDb();
if (!db) throw new Error("DB not available");
if (data.length === 0) return;
await db.insert(frames).values(data);
}
export async function listFrames(projectId: number) {
const db = await getDb();
if (!db) return [];
return db.select().from(frames).where(eq(frames.projectId, projectId)).orderBy(asc(frames.frameIndex));
}
export async function getFrame(projectId: number, frameIndex: number) {
const db = await getDb();
if (!db) return null;
const result = await db.select().from(frames)
.where(and(eq(frames.projectId, projectId), eq(frames.frameIndex, frameIndex)))
.limit(1);
return result[0] ?? null;
}
export async function updateFrame(id: number, data: Partial<InsertFrame>) {
const db = await getDb();
if (!db) return;
await db.update(frames).set(data).where(eq(frames.id, id));
}
// ============ LAYERS ============
export async function createLayer(data: InsertLayer) {
const db = await getDb();
if (!db) throw new Error("DB not available");
const result = await db.insert(layers).values(data);
return { id: result[0].insertId };
}
export async function listLayers(projectId: number) {
const db = await getDb();
if (!db) return [];
return db.select().from(layers).where(eq(layers.projectId, projectId)).orderBy(asc(layers.order));
}
export async function updateLayer(id: number, data: Partial<InsertLayer>) {
const db = await getDb();
if (!db) return;
await db.update(layers).set(data).where(eq(layers.id, id));
}
export async function deleteLayer(id: number) {
const db = await getDb();
if (!db) return;
await db.delete(layers).where(eq(layers.id, id));
}
// ============ CHARACTERS ============
export async function createCharacter(data: InsertCharacter) {
const db = await getDb();
if (!db) throw new Error("DB not available");
const result = await db.insert(characters).values(data);
return { id: result[0].insertId };
}
export async function listCharacters(projectId: number) {
const db = await getDb();
if (!db) return [];
return db.select().from(characters).where(eq(characters.projectId, projectId));
}
export async function updateCharacter(id: number, data: Partial<InsertCharacter>) {
const db = await getDb();
if (!db) return;
await db.update(characters).set(data).where(eq(characters.id, id));
}
// ============ AI ENGINES ============
export async function createAiEngine(data: InsertAiEngine) {
const db = await getDb();
if (!db) throw new Error("DB not available");
const result = await db.insert(aiEngines).values(data);
return { id: result[0].insertId };
}
export async function listAiEngines() {
const db = await getDb();
if (!db) return [];
return db.select().from(aiEngines).orderBy(desc(aiEngines.createdAt));
}
export async function updateAiEngine(id: number, data: Partial<InsertAiEngine>) {
const db = await getDb();
if (!db) return;
await db.update(aiEngines).set(data).where(eq(aiEngines.id, id));
}
export async function deleteAiEngine(id: number) {
const db = await getDb();
if (!db) return;
await db.delete(aiEngines).where(eq(aiEngines.id, id));
}
export async function getDefaultEngine(taskType: string) {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(aiEngines)
.where(and(eq(aiEngines.taskType, taskType as any), eq(aiEngines.isDefault, true), eq(aiEngines.isActive, true)))
.limit(1);
return result[0];
}
// ============ GENERATION JOBS ============
export async function createGenerationJob(data: InsertGenerationJob) {
const db = await getDb();
if (!db) throw new Error("DB not available");
const result = await db.insert(generationJobs).values(data);
return { id: result[0].insertId };
}
export async function listGenerationJobs(projectId: number) {
const db = await getDb();
if (!db) return [];
return db.select().from(generationJobs).where(eq(generationJobs.projectId, projectId)).orderBy(desc(generationJobs.createdAt));
}
export async function updateGenerationJob(id: number, data: Partial<InsertGenerationJob>) {
const db = await getDb();
if (!db) return;
await db.update(generationJobs).set(data).where(eq(generationJobs.id, id));
}
// ============ ASSISTANT MESSAGES ============
export async function createAssistantMessage(data: InsertAssistantMessage) {
const db = await getDb();
if (!db) throw new Error("DB not available");
await db.insert(assistantMessages).values(data);
}
export async function listAssistantMessages(projectId: number) {
const db = await getDb();
if (!db) return [];
return db.select().from(assistantMessages).where(eq(assistantMessages.projectId, projectId)).orderBy(asc(assistantMessages.createdAt));
}