import 'dotenv/config'; import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import morgan from 'morgan'; import rateLimit from 'express-rate-limit'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); import authRouter from './routes/auth.js'; import investisseursRouter from './routes/investisseurs.js'; import plateformesRouter from './routes/plateformes.js'; import categoriesRouter from './routes/categories.js'; import depotsRetraitsRouter from './routes/depotsRetraits.js'; import investissementsRouter from './routes/investissements.js'; import investissementsPeRouter from './routes/investissementsPe.js'; import remboursementsRouter from './routes/remboursements.js'; import simulRouter from './routes/simul.js'; import dashboardRouter from './routes/dashboard.js'; import dashboardPeRouter from './routes/dashboardPe.js'; import taxreportRouter from './routes/taxreport.js'; import taxreportPeRouter from './routes/taxreportPe.js'; import platefomeTaxRouter from './routes/plateforme-tax.js'; import importsRouter from './routes/imports.js'; import pfuRouter from './routes/pfu.js'; import notationRouter from './routes/notation.js'; import garantiesRouter from './routes/garanties.js'; import objectifsRouter from './routes/objectifs.js'; import reinvestissementsRouter from './routes/reinvestissements.js'; import fraisOperationsRouter from './routes/fraisOperations.js'; import correctionsRouter from './routes/corrections.js'; import comptesRouter from './routes/comptes.js'; import preferencesRouter from './routes/preferences.js'; import iconsRouter from './routes/icons.js'; import { errorHandler } from './middleware/errorHandler.js'; import { requireAuth, requireAdmin } from './middleware/auth.js'; import { startAutoStatutJob } from './jobs/autoStatut.js'; import { startAutoExportJob } from './jobs/autoExport.js'; import { startAutoTicketStatusJob } from './jobs/autoTicketStatus.js'; import { startAutoCleanNotifsJob } from './jobs/autoCleanNotifs.js'; import { startCheckDonneesIncompletesJob } from './jobs/checkDonneesIncompletes.js'; import adminRouter from './routes/admin.js'; import invitationsRouter from './routes/invitations.js'; import auditLogsRouter from './routes/auditLogs.js'; import smtpRouter from './routes/smtp.js'; import generalRouter from './routes/general.js'; import adminAiRouter from './routes/adminAi.js'; import tauxCreditImpotRouter from './routes/tauxCreditImpot.js'; import referentielRouter from './routes/referentiel.js'; import referentielPublicRouter from './routes/referentielPublic.js'; import institutionsReferentielRouter from './routes/institutionsReferentiel.js'; import institutionsReferentielPublicRouter from './routes/institutionsReferentielPublic.js'; import refCategoriesInstitutionsRouter from './routes/ref-categories-institutions.js'; import enveloppesReferentielRouter from './routes/enveloppesReferentiel.js'; import enveloppesReferentielPublicRouter from './routes/enveloppesReferentielPublic.js'; import supportsReferentielRouter from './routes/supportsReferentiel.js'; import empruntsRouter from './routes/emprunts.js'; import refCategoriesEnveloppesRouter from './routes/ref-categories-enveloppes.js'; import refCategoriesRouter from './routes/ref-categories.js'; import refSecteursRouter from './routes/ref-secteurs.js'; import categoriesInvRouter from './routes/categories-inv.js'; import secteursInvRouter from './routes/secteurs-inv.js'; import associationsInvRouter from './routes/associations-inv.js'; import notificationsRouter from './routes/notifications.js'; import ticketsRouter from './routes/tickets.js'; import apiKeysRouter from './routes/apiKeys.js'; import v1Router from './routes/v1/index.js'; import documentsRouter from './routes/documents.js'; import aiRouter from './routes/ai.js'; import workspacesRouter from './routes/workspaces.js'; import adminWorkspacesRouter from './routes/adminWorkspaces.js'; import { requireApiKey } from './middleware/apiKey.js'; import { swaggerSpec, swaggerUi } from './swagger.js'; import db from './db/index.js'; import { getSmtpConfig } from './utils/mailer.js'; const app = express(); const PORT = process.env.PORT || 4000; app.use(helmet({ crossOriginResourcePolicy: { policy: 'cross-origin' } })); app.use(cors()); app.use(express.json({ limit: '5mb' })); app.use(morgan('dev')); // ── Logos plateformes — servis sans authentification ───────────────────── const _dataDir = process.env.DATA_DIR ? path.resolve(process.env.DATA_DIR) : path.resolve(__dirname, '../../data'); // fallback dev : 2 niveaux au-dessus de src/ const logosDir = path.join(_dataDir, 'logos'); app.use('/api/logos', express.static(logosDir, { maxAge: '1d', fallthrough: false })); const iconsDir2 = path.join(_dataDir, 'icons'); app.use('/api/icons-files', express.static(iconsDir2, { maxAge: '1h', fallthrough: false })); // Basic rate-limit on auth endpoints // Seuil relevé (50 → 300) : usage local mono-utilisateur/multi-investisseur avec // beaucoup d'allers-retours de test (login, 2FA, création de comptes admin/invitation…), // l'ancien seuil de 50 req/15min était atteint trop facilement en usage normal. const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 300, standardHeaders: true, legacyHeaders: false, }); app.get('/api/health', (_, res) => res.json({ ok: true, ts: new Date().toISOString() })); // Informations publiques de la plateforme (utilisées sur la page de login) app.get('/api/app-info', (_, res) => { try { const cfg = getSmtpConfig(); const icon = db.prepare(`SELECT filename FROM app_icons WHERE name = 'logo-app' LIMIT 1`).get(); const row = db.prepare('SELECT allow_registration, min_password_length, mcp_url FROM smtp_config WHERE id = 1').get(); // Thème de couleur (Admin > Général) : appliqué à TOUS les utilisateurs, // y compris sur la page de connexion (avant authentification) — d'où son // exposition ici, comme allowRegistration/mcpUrl déjà publics. const theme = db.prepare('SELECT color_theme FROM parametres_theme WHERE id = 1').get(); res.json({ appName: cfg.appName || 'Crowdlending Tracker', iconUrl: icon ? `/api/icons-files/${icon.filename}` : null, allowRegistration: row ? row.allow_registration !== 0 : true, minPasswordLength: row ? (row.min_password_length || 8) : 8, mcpUrl: row ? (row.mcp_url || '') : '', colorTheme: theme?.color_theme || 'classique', }); } catch { res.json({ appName: 'Crowdlending Tracker', iconUrl: null, allowRegistration: true, minPasswordLength: 8, mcpUrl: '', colorTheme: 'classique' }); } }); app.use('/api/auth', authLimiter, authRouter); // ── API publique v1 (lecture seule, authentification par clé API) ──────── // Montée ici, avant les routes internes ci-dessous : la ligne // `app.use('/api', requireAuth, associationsInvRouter)` plus bas capte tout // préfixe /api/* avec le JWT interne, ce qui intercepterait /api/v1/* et // /api/docs si elles étaient montées après. app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); app.get('/api/openapi.json', (_, res) => res.json(swaggerSpec)); app.use('/api/v1', requireApiKey, v1Router); // All routes below require authentication app.use('/api/investisseurs', requireAuth, investisseursRouter); app.use('/api/plateformes', requireAuth, plateformesRouter); app.use('/api/categories', requireAuth, categoriesRouter); app.use('/api/depots-retraits', requireAuth, depotsRetraitsRouter); app.use('/api/investissements', requireAuth, investissementsRouter); app.use('/api/investissements-pe', requireAuth, investissementsPeRouter); app.use('/api/remboursements', requireAuth, remboursementsRouter); app.use('/api/simul', requireAuth, simulRouter); app.use('/api/dashboard', requireAuth, dashboardRouter); app.use('/api/dashboard-pe', requireAuth, dashboardPeRouter); app.use('/api/taxreport', requireAuth, taxreportRouter); app.use('/api/taxreport-pe', requireAuth, taxreportPeRouter); app.use('/api/plateforme-tax', requireAuth, platefomeTaxRouter); app.use('/api/imports', requireAuth, importsRouter); app.use('/api/pfu', requireAuth, pfuRouter); app.use('/api/notation', requireAuth, notationRouter); app.use('/api/garanties', requireAuth, garantiesRouter); app.use('/api/objectifs', requireAuth, objectifsRouter); app.use('/api/reinvestissements', requireAuth, reinvestissementsRouter); app.use('/api/frais-operations', requireAuth, fraisOperationsRouter); app.use('/api/corrections', requireAuth, correctionsRouter); app.use('/api/comptes', requireAuth, comptesRouter); app.use('/api/emprunts', requireAuth, empruntsRouter); app.use('/api/preferences', requireAuth, preferencesRouter); app.use('/api/icons', requireAuth, iconsRouter); app.use('/api/admin', requireAuth, requireAdmin, adminRouter); app.use('/api/admin/audit-logs', requireAuth, requireAdmin, auditLogsRouter); app.use('/api/admin/smtp', requireAuth, requireAdmin, smtpRouter); app.use('/api/admin/general', requireAuth, requireAdmin, generalRouter); app.use('/api/admin/ai', requireAuth, requireAdmin, adminAiRouter); // Invitations : routes admin protégées + routes publiques (register/validate) app.use('/api/admin/invitations', requireAuth, requireAdmin, invitationsRouter); app.use('/api/invitations', invitationsRouter); app.use('/api/taux-credit-impot', requireAuth, tauxCreditImpotRouter); app.use('/api/referentiel', requireAuth, requireAdmin, referentielRouter); app.use('/api/referentiel-public', requireAuth, referentielPublicRouter); // Chantier "Référentiel Institutions financières" (26/09/26, demande Olivier) — Phase 1 : // catalogue admin transverse aux workspaces, cf. institutionsReferentiel.js. app.use('/api/institutions-referentiel', requireAuth, requireAdmin, institutionsReferentielRouter); app.use('/api/institutions-referentiel-public', requireAuth, institutionsReferentielPublicRouter); app.use('/api/ref-categories-institutions', requireAuth, requireAdmin, refCategoriesInstitutionsRouter); // Chantier "Référentiel Enveloppes & Supports d'épargne" (28/09/26, demande Olivier) — // catalogue admin transverse aux workspaces, catalogue seul pour l'instant (pas de // rattachement à l'existant), cf. enveloppesReferentiel.js. app.use('/api/enveloppes-referentiel', requireAuth, requireAdmin, enveloppesReferentielRouter); app.use('/api/enveloppes-referentiel-public', requireAuth, enveloppesReferentielPublicRouter); app.use('/api/supports-referentiel', requireAuth, requireAdmin, supportsReferentielRouter); app.use('/api/ref-categories-enveloppes', requireAuth, requireAdmin, refCategoriesEnveloppesRouter); app.use('/api/ref-categories', requireAuth, requireAdmin, refCategoriesRouter); app.use('/api/ref-secteurs', requireAuth, requireAdmin, refSecteursRouter); app.use('/api/categories-inv', requireAuth, categoriesInvRouter); app.use('/api/secteurs-inv', requireAuth, secteursInvRouter); app.use('/api', requireAuth, associationsInvRouter); app.use('/api/notifications', notificationsRouter); app.use('/api/tickets', requireAuth, ticketsRouter); app.use('/api/api-keys', requireAuth, apiKeysRouter); app.use('/api/documents', requireAuth, documentsRouter); app.use('/api/ai', requireAuth, aiRouter); app.use('/api/workspaces', requireAuth, workspacesRouter); app.use('/api/admin/workspaces', requireAuth, requireAdmin, adminWorkspacesRouter); app.use(errorHandler); app.listen(PORT, () => { console.log(`Crowdlending API listening on http://localhost:${PORT}`); startAutoStatutJob(); startAutoExportJob(); startAutoTicketStatusJob(); startAutoCleanNotifsJob(); startCheckDonneesIncompletesJob(); });