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>
271 lines
9.3 KiB
TypeScript
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));
|
|
}
|