File size: 9,554 Bytes
f20ac25 cc442ef 7cf001f c8a4f4b 9cc0a90 b150436 c8a4f4b 617b081 ce3c4bc c8a4f4b ce3c4bc 617b081 ce3c4bc cc442ef cca255d cc442ef 04b12d1 cc442ef a7a1900 04b12d1 cc442ef b150436 cc442ef c8a4f4b 04b12d1 9595ade 9cc0a90 04b12d1 2d7440e 04b12d1 b74fcbf 8577fe3 d06feaa 8577fe3 d9879cf 8577fe3 b924b46 d9879cf b924b46 7d1fb38 cc442ef fb2b5c3 eac938a fb2b5c3 eac938a fb2b5c3 eac938a fb2b5c3 eac938a fb2b5c3 eac938a fb2b5c3 eac938a fb2b5c3 eac938a fb2b5c3 04b12d1 cc442ef a7a1900 0de1348 4a79a6d a59f28c cc442ef 7d1fb38 cc442ef | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | 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();
|