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();