Fonctionnalités implémentées : ✅ Authentification locale (email/password) + Azure AD + Manus OAuth ✅ Upload drag-and-drop de fichiers PDF avec suivi en temps réel ✅ Extraction automatique avec Mistral AI (OCR + LLM) ✅ Détection de doublons (fournisseur, numéro, date) ✅ Score de qualité d'extraction (0-100) ✅ Tableau de bord avec statistiques ✅ Liste des factures avec recherche et filtres ✅ Paramètres utilisateur (LLM, keywords, SFTP) ✅ Historique des imports avec logs détaillés ✅ Gestion des utilisateurs (admin) ✅ Export SFTP manuel/automatique ✅ Stockage local avec organisation YYYY-MM ✅ Tests unitaires d'authentification Architecture : - Frontend : React 19 + Vite + TailwindCSS + Radix UI - Backend : Express + tRPC + Drizzle ORM - Base de données : MySQL (6 tables) - IA : Mistral AI pour extraction - Stockage : Local filesystem - Export : SFTP Pages : - Login (choix local/Azure/Manus) - Dashboard (statistiques) - Upload (drag-and-drop) - Invoices (liste avec recherche) - Settings (LLM, keywords, SFTP) - History (logs d'import) - Users (gestion admin)
112 lines
3.1 KiB
TypeScript
112 lines
3.1 KiB
TypeScript
import fs from "fs/promises";
|
|
import path from "path";
|
|
import { nanoid } from "nanoid";
|
|
|
|
// Storage base path (local filesystem)
|
|
const STORAGE_BASE_PATH = process.env.STORAGE_BASE_PATH || path.join(process.cwd(), "storage");
|
|
|
|
/**
|
|
* Ensure storage directory exists
|
|
*/
|
|
async function ensureStorageDir(dirPath: string) {
|
|
try {
|
|
await fs.mkdir(dirPath, { recursive: true });
|
|
} catch (error) {
|
|
console.error(`[LocalStorage] Failed to create directory ${dirPath}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a storage key with YYYY-MM prefix for organization
|
|
*/
|
|
export function generateStorageKey(userId: number, fileName: string): string {
|
|
const now = new Date();
|
|
const year = now.getFullYear();
|
|
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
const randomId = nanoid(8);
|
|
|
|
// Format: YYYY-MM/userId-randomId-filename
|
|
return `${year}-${month}/${userId}-${randomId}-${fileName}`;
|
|
}
|
|
|
|
/**
|
|
* Store a file in local storage
|
|
* @param fileKey - Storage key (e.g., "2025-01/1-abc123-invoice.pdf")
|
|
* @param buffer - File content as Buffer
|
|
* @param contentType - MIME type (optional, for metadata)
|
|
* @returns Object with key and public URL
|
|
*/
|
|
export async function localStoragePut(
|
|
fileKey: string,
|
|
buffer: Buffer,
|
|
contentType?: string
|
|
): Promise<{ key: string; url: string }> {
|
|
try {
|
|
const fullPath = path.join(STORAGE_BASE_PATH, fileKey);
|
|
const dirPath = path.dirname(fullPath);
|
|
|
|
// Ensure directory exists
|
|
await ensureStorageDir(dirPath);
|
|
|
|
// Write file
|
|
await fs.writeFile(fullPath, buffer);
|
|
|
|
// Generate public URL (served by Express static middleware)
|
|
const url = `/storage/${fileKey}`;
|
|
|
|
console.log(`[LocalStorage] File stored: ${fileKey}`);
|
|
|
|
return { key: fileKey, url };
|
|
} catch (error) {
|
|
console.error(`[LocalStorage] Failed to store file ${fileKey}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve a file from local storage
|
|
* @param fileKey - Storage key
|
|
* @returns File content as Buffer
|
|
*/
|
|
export async function localStorageGet(fileKey: string): Promise<Buffer> {
|
|
try {
|
|
const fullPath = path.join(STORAGE_BASE_PATH, fileKey);
|
|
const buffer = await fs.readFile(fullPath);
|
|
return buffer;
|
|
} catch (error) {
|
|
console.error(`[LocalStorage] Failed to retrieve file ${fileKey}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a file from local storage
|
|
* @param fileKey - Storage key
|
|
*/
|
|
export async function localStorageDelete(fileKey: string): Promise<void> {
|
|
try {
|
|
const fullPath = path.join(STORAGE_BASE_PATH, fileKey);
|
|
await fs.unlink(fullPath);
|
|
console.log(`[LocalStorage] File deleted: ${fileKey}`);
|
|
} catch (error) {
|
|
console.error(`[LocalStorage] Failed to delete file ${fileKey}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a file exists in storage
|
|
* @param fileKey - Storage key
|
|
* @returns true if file exists, false otherwise
|
|
*/
|
|
export async function localStorageExists(fileKey: string): Promise<boolean> {
|
|
try {
|
|
const fullPath = path.join(STORAGE_BASE_PATH, fileKey);
|
|
await fs.access(fullPath);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|