edtech / apps /api /src /index.ts
CognxSafeTrack
chore: execute Sprint 38 technical debt resolution (Type Safety, Zod validation, Vitest, Mock LLM extracted)
d9879cf
import dns from 'node:dns';
dns.setDefaultResultOrder('ipv4first');
import Fastify from 'fastify';
import cors from '@fastify/cors';
import { whatsappRoutes } from './routes/whatsapp';
import { adminRoutes } from './routes/admin';
import { aiRoutes } from './routes/ai';
import { paymentRoutes, stripeWebhookRoute } from './routes/payments';
import { internalRoutes } from './routes/internal';
import { studentRoutes } from './routes/student';
// ── Fail-fast: vérifier les secrets critiques au démarrage ─────────────────────
// Only WHATSAPP_VERIFY_TOKEN is strictly needed at startup (for Meta webhook validation).
// All other secrets are validated lazily (guarded routes return 503 if missing).
const REQUIRED_ENV = ['WHATSAPP_VERIFY_TOKEN'];
const WARN_ENV = ['ADMIN_API_KEY', 'WHATSAPP_APP_SECRET'];
for (const key of REQUIRED_ENV) {
if (!process.env[key]) {
console.error(`[STARTUP] ❌ Missing required environment variable: ${key}`);
process.exit(1);
}
}
for (const key of WARN_ENV) {
if (!process.env[key]) {
console.warn(`[STARTUP] ⚠️ ${key} not set — related features will be degraded`);
}
}
const server = Fastify({
logger: true,
ignoreTrailingSlash: true
});
// ── CORS ───────────────────────────────────────────────────────────────────────
server.register(cors);
// ── Rate Limiting (lazy import — package installed at runtime on HF) ───────────
async function setupRateLimit() {
try {
const rateLimit = await import('@fastify/rate-limit');
await server.register(rateLimit.default, {
global: true,
max: 300,
timeWindow: '1 minute',
});
console.log('[RATE-LIMIT] Rate limiting enabled (300 req/min global)');
} catch {
console.warn('[RATE-LIMIT] @fastify/rate-limit not available — skipping');
}
}
// ── Public Routes (no auth) ────────────────────────────────────────────────────
server.register(whatsappRoutes, { prefix: '/v1/whatsapp' });
server.register(studentRoutes, { prefix: '/v1/student' });
// ── Stripe Webhook (public — Stripe can't send API Key, verified by signature) ─
server.register(stripeWebhookRoute, { prefix: '/v1/payments' });
// ── Private Routes (require ADMIN_API_KEY) ─────────────────────────────────────
server.register(async function guardedRoutes(scope) {
scope.addHook('onRequest', async (request, reply) => {
const apiKey = process.env.ADMIN_API_KEY;
if (!apiKey) {
request.log.error('ADMIN_API_KEY is not configured!');
return reply.code(503).send({ error: 'Service misconfigured' });
}
const authHeader = request.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return reply.code(401).send({ error: 'Unauthorized', message: 'Missing Authorization header' });
}
const token = authHeader.slice(7);
if (token !== apiKey) {
return reply.code(401).send({ error: 'Unauthorized', message: 'Invalid API key' });
}
});
scope.register(adminRoutes, { prefix: '/v1/admin' });
scope.register(aiRoutes, { prefix: '/v1/ai' });
scope.register(paymentRoutes, { prefix: '/v1/payments' });
scope.register(internalRoutes);
});
// ── Health Routes (public) ─────────────────────────────────────────────────────
server.get('/', async (_req, reply) => {
return reply.code(200).type('application/json').send({ ok: true });
});
server.get('/debug/net', async (_req, reply) => {
try {
const res = await fetch('https://www.google.com', { method: 'GET' });
return reply.send({ ok: true, status: res.status });
} catch (e: unknown) {
return reply.code(500).send({ ok: false, error: (e as any)?.message || String(e) });
}
});
server.get('/debug/graph', async (_req, reply) => {
try {
const res = await fetch('https://graph.facebook.com', { method: 'GET' });
return reply.send({ ok: true, status: res.status });
} catch (e: unknown) {
return reply.code(500).send({ ok: false, error: (e as any)?.message || String(e) });
}
});
server.get('/health', async () => {
return { status: 'ok', timestamp: new Date().toISOString() };
});
// ── Privacy Policy (required by Meta for app publication) ──────────────────────
server.get('/v1/privacy', async (_req, reply) => {
const html = `<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Politique de Confidentialité — XAMLÉ Studio</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 760px; margin: 40px auto; padding: 0 20px; color: #1e293b; line-height: 1.7; }
h1 { font-size: 2rem; margin-bottom: 4px; }
h2 { font-size: 1.2rem; margin-top: 2rem; color: #0f172a; }
p, li { color: #334155; }
a { color: #059669; }
.updated { color: #94a3b8; font-size: 0.9rem; }
</style>
</head>
<body>
<h1>Politique de Confidentialité</h1>
<p class="updated">Dernière mise à jour : 7 mars 2026</p>
<h2>1. Qui sommes-nous ?</h2>
<p>XAMLÉ Studio est une plateforme d'éducation entrepreneuriale accessible via WhatsApp, opérée par xamlé.studio. Contact : <a href="mailto:contact@xamle.studio">contact@xamle.studio</a></p>
<h2>2. Données collectées</h2>
<p>Lors de votre inscription et utilisation du service, nous collectons :</p>
<ul>
<li>Votre numéro de téléphone WhatsApp</li>
<li>La langue choisie (Français ou Wolof)</li>
<li>Votre secteur d'activité / projet professionnel (fourni volontairement)</li>
<li>Vos réponses aux exercices et contenus pédagogiques</li>
<li>Les métadonnées de vos messages (horodatage, type de message)</li>
</ul>
<h2>3. Utilisation des données</h2>
<p>Vos données sont utilisées pour :</p>
<ul>
<li>Vous envoyer des leçons quotidiennes personnalisées via WhatsApp</li>
<li>Personnaliser le contenu pédagogique à votre secteur d'activité</li>
<li>Générer des documents AI (One-Pager PDF, Pitch Deck) basés sur votre parcours</li>
<li>Améliorer la qualité de nos formations</li>
<li>Traiter vos paiements pour les formations premium</li>
</ul>
<h2>4. Partage des données</h2>
<p>Nous ne vendons jamais vos données. Elles peuvent être partagées uniquement avec :</p>
<ul>
<li><strong>Meta / WhatsApp</strong> — pour l'acheminement des messages</li>
<li><strong>OpenAI</strong> — pour la génération et personnalisation du contenu pédagogique</li>
<li><strong>Stripe</strong> — pour le traitement sécurisé des paiements</li>
<li><strong>Cloudflare</strong> — pour le stockage des documents générés</li>
</ul>
<h2>5. Conservation des données</h2>
<p>Vos données sont conservées pendant toute la durée de votre inscription active. Vous pouvez demander la suppression de vos données à tout moment en envoyant un e-mail à <a href="mailto:contact@xamle.studio">contact@xamle.studio</a>.</p>
<h2>6. Vos droits</h2>
<p>Conformément au RGPD et aux lois applicables, vous disposez du droit de :</p>
<ul>
<li>Accéder à vos données personnelles</li>
<li>Corriger les données inexactes</li>
<li>Demander la suppression de vos données</li>
<li>Vous opposer au traitement de vos données</li>
</ul>
<p>Pour exercer ces droits, contactez-nous à : <a href="mailto:contact@xamle.studio">contact@xamle.studio</a></p>
<h2>7. Sécurité</h2>
<p>Vos données sont protégées par chiffrement (TLS en transit, AES au repos). L'accès aux données est strictement limité aux systèmes nécessaires au fonctionnement du service.</p>
<h2>8. Modifications</h2>
<p>Cette politique peut être mise à jour. En cas de modification majeure, vous serez informé via WhatsApp.</p>
<h2>9. Contact</h2>
<p>Pour toute question relative à cette politique : <a href="mailto:contact@xamle.studio">contact@xamle.studio</a></p>
</body>
</html>`;
return reply.code(200).type('text/html').send(html);
});
// ── Start Server ───────────────────────────────────────────────────────────────
const start = async () => {
try {
await setupRateLimit();
const port = parseInt(process.env.PORT || '8080');
const isGateway = process.env.IS_GATEWAY === 'true' || process.env.HF_SPACE_ID !== undefined;
console.log(`[STARTUP] Mode: ${isGateway ? 'GATEWAY (Forwarding Only)' : 'DIRECT (Processing)'}`);
console.log(`[STARTUP] Forwarding to: ${process.env.RAILWAY_INTERNAL_URL || 'NONE'}`);
await server.listen({ port, host: '0.0.0.0' });
console.log(`Server listening on http://0.0.0.0:${port}`);
} catch (err) {
console.error(err);
process.exit(1);
}
};
start();