CognxSafeTrack commited on
Commit ·
c8a4f4b
1
Parent(s): e73f123
fix(security): audit fixes — Stripe webhook scope, fail-fast secrets, prisma singleton, worker auth, Zod validation, WA number env var
Browse files- .env.example +38 -0
- apps/api/src/index.ts +13 -2
- apps/api/src/routes/admin.ts +1 -2
- apps/api/src/routes/payments.ts +34 -20
- apps/api/src/services/stripe.ts +7 -3
- apps/web/src/App.tsx +4 -2
- apps/web/src/vite-env.d.ts +11 -0
- apps/whatsapp-worker/src/index.ts +26 -9
.env.example
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Environment Variables — EdTech
|
| 2 |
+
|
| 3 |
+
# Copy this file to .env and fill in the values.
|
| 4 |
+
# NEVER commit the real .env file to git.
|
| 5 |
+
|
| 6 |
+
# ─── API Auth ──────────────────────────────────────────────────────────────────
|
| 7 |
+
ADMIN_API_KEY= # Strong random secret, e.g. openssl rand -hex 32
|
| 8 |
+
|
| 9 |
+
# ─── WhatsApp / Meta ───────────────────────────────────────────────────────────
|
| 10 |
+
WHATSAPP_VERIFY_TOKEN= # Token you set in the Meta App dashboard
|
| 11 |
+
WHATSAPP_APP_SECRET= # App Secret from Meta App Settings > Basic
|
| 12 |
+
WHATSAPP_ACCESS_TOKEN= # Permanent System User token from Meta Business
|
| 13 |
+
|
| 14 |
+
# ─── Database ──────────────────────────────────────────────────────────────────
|
| 15 |
+
DATABASE_URL=postgresql://user:password@localhost:5432/edtech?schema=public
|
| 16 |
+
|
| 17 |
+
# ─── Redis ─────────────────────────────────────────────────────────────────────
|
| 18 |
+
REDIS_URL= # e.g. redis://default:password@host:6379
|
| 19 |
+
# Or individual connection params (if REDIS_URL is not set):
|
| 20 |
+
# REDIS_HOST=localhost
|
| 21 |
+
# REDIS_PORT=6379
|
| 22 |
+
# REDIS_USERNAME=default
|
| 23 |
+
# REDIS_PASSWORD=
|
| 24 |
+
# REDIS_TLS=false
|
| 25 |
+
|
| 26 |
+
# ─── Stripe ────────────────────────────────────────────────────────────────────
|
| 27 |
+
STRIPE_SECRET_KEY= # sk_live_... (or sk_test_... for dev)
|
| 28 |
+
STRIPE_WEBHOOK_SECRET= # whsec_... from Stripe dashboard > Webhooks
|
| 29 |
+
|
| 30 |
+
# ─── OpenAI / AI ───────────────────────────────────────────────────────────────
|
| 31 |
+
OPENAI_API_KEY= # sk-...
|
| 32 |
+
|
| 33 |
+
# ─── Frontend ──────────────────────────────────────────────────────────────────
|
| 34 |
+
VITE_CLIENT_URL=https://your-frontend.netlify.app
|
| 35 |
+
VITE_WHATSAPP_NUMBER=221771234567 # Without + prefix, for wa.me links
|
| 36 |
+
|
| 37 |
+
# ─── Internal (Worker → API) ───────────────────────────────────────────────────
|
| 38 |
+
API_URL=http://localhost:3001 # In prod: full URL of the Fastify API
|
apps/api/src/index.ts
CHANGED
|
@@ -3,7 +3,16 @@ import cors from '@fastify/cors';
|
|
| 3 |
import { whatsappRoutes } from './routes/whatsapp';
|
| 4 |
import { adminRoutes } from './routes/admin';
|
| 5 |
import { aiRoutes } from './routes/ai';
|
| 6 |
-
import { paymentRoutes } from './routes/payments';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
const server = Fastify({
|
| 9 |
logger: true,
|
|
@@ -31,8 +40,10 @@ async function setupRateLimit() {
|
|
| 31 |
// ── Public Routes (no auth) ────────────────────────────────────────────────────
|
| 32 |
server.register(whatsappRoutes, { prefix: '/v1/whatsapp' });
|
| 33 |
|
|
|
|
|
|
|
|
|
|
| 34 |
// ── Private Routes (require ADMIN_API_KEY) ─────────────────────────────────────
|
| 35 |
-
// Inline addHook on guardedRoutes scope — properly encapsulated in Fastify v4
|
| 36 |
server.register(async function guardedRoutes(scope) {
|
| 37 |
scope.addHook('onRequest', async (request, reply) => {
|
| 38 |
const apiKey = process.env.ADMIN_API_KEY;
|
|
|
|
| 3 |
import { whatsappRoutes } from './routes/whatsapp';
|
| 4 |
import { adminRoutes } from './routes/admin';
|
| 5 |
import { aiRoutes } from './routes/ai';
|
| 6 |
+
import { paymentRoutes, stripeWebhookRoute } from './routes/payments';
|
| 7 |
+
|
| 8 |
+
// ── Fail-fast: vérifier les secrets critiques au démarrage ─────────────────────
|
| 9 |
+
const REQUIRED_ENV = ['ADMIN_API_KEY', 'WHATSAPP_APP_SECRET', 'WHATSAPP_VERIFY_TOKEN'];
|
| 10 |
+
for (const key of REQUIRED_ENV) {
|
| 11 |
+
if (!process.env[key]) {
|
| 12 |
+
console.error(`[STARTUP] ❌ Missing required environment variable: ${key}`);
|
| 13 |
+
process.exit(1);
|
| 14 |
+
}
|
| 15 |
+
}
|
| 16 |
|
| 17 |
const server = Fastify({
|
| 18 |
logger: true,
|
|
|
|
| 40 |
// ── Public Routes (no auth) ────────────────────────────────────────────────────
|
| 41 |
server.register(whatsappRoutes, { prefix: '/v1/whatsapp' });
|
| 42 |
|
| 43 |
+
// ── Stripe Webhook (public — Stripe can't send API Key, verified by signature) ─
|
| 44 |
+
server.register(stripeWebhookRoute, { prefix: '/v1/payments' });
|
| 45 |
+
|
| 46 |
// ── Private Routes (require ADMIN_API_KEY) ─────────────────────────────────────
|
|
|
|
| 47 |
server.register(async function guardedRoutes(scope) {
|
| 48 |
scope.addHook('onRequest', async (request, reply) => {
|
| 49 |
const apiKey = process.env.ADMIN_API_KEY;
|
apps/api/src/routes/admin.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
import { FastifyInstance } from 'fastify';
|
| 2 |
-
import {
|
| 3 |
|
| 4 |
-
const prisma = new PrismaClient();
|
| 5 |
|
| 6 |
export async function adminRoutes(fastify: FastifyInstance) {
|
| 7 |
// 1. Get Dashboard Stats
|
|
|
|
| 1 |
import { FastifyInstance } from 'fastify';
|
| 2 |
+
import { prisma } from '../services/prisma';
|
| 3 |
|
|
|
|
| 4 |
|
| 5 |
export async function adminRoutes(fastify: FastifyInstance) {
|
| 6 |
// 1. Get Dashboard Stats
|
apps/api/src/routes/payments.ts
CHANGED
|
@@ -1,20 +1,26 @@
|
|
| 1 |
import { FastifyInstance } from 'fastify';
|
| 2 |
import { stripeService } from '../services/stripe';
|
|
|
|
|
|
|
| 3 |
|
| 4 |
-
//
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
| 7 |
|
|
|
|
| 8 |
export async function paymentRoutes(fastify: FastifyInstance) {
|
| 9 |
|
| 10 |
// Create a Checkout Session
|
| 11 |
fastify.post('/checkout', async (request, reply) => {
|
| 12 |
-
const
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
return reply.status(400).send({ error: 'Missing userId or trackId' });
|
| 16 |
}
|
| 17 |
|
|
|
|
|
|
|
| 18 |
try {
|
| 19 |
// Validate the track exists and is premium
|
| 20 |
const track = await prisma.track.findUnique({ where: { id: trackId } });
|
|
@@ -42,10 +48,21 @@ export async function paymentRoutes(fastify: FastifyInstance) {
|
|
| 42 |
return reply.status(500).send({ error: 'Failed to create checkout session' });
|
| 43 |
}
|
| 44 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
/
|
| 47 |
-
// Note: We need the raw body to verify the signature. Fastify requires specific config for this.
|
| 48 |
-
fastify.post('/webhook', { config: { rawBody: true } }, async (request, reply) => {
|
| 49 |
const sig = request.headers['stripe-signature'];
|
| 50 |
|
| 51 |
if (!sig || typeof sig !== 'string') {
|
|
@@ -55,9 +72,10 @@ export async function paymentRoutes(fastify: FastifyInstance) {
|
|
| 55 |
let event;
|
| 56 |
|
| 57 |
try {
|
| 58 |
-
|
| 59 |
-
event = stripeService.verifyWebhookSignature(
|
| 60 |
} catch (err: any) {
|
|
|
|
| 61 |
return reply.status(400).send(`Webhook Error: ${err.message}`);
|
| 62 |
}
|
| 63 |
|
|
@@ -71,7 +89,6 @@ export async function paymentRoutes(fastify: FastifyInstance) {
|
|
| 71 |
|
| 72 |
if (userId && trackId) {
|
| 73 |
try {
|
| 74 |
-
// Start a transaction: Record payment and Create Enrollment
|
| 75 |
await prisma.$transaction(async (tx: any) => {
|
| 76 |
// 1. Record the payment
|
| 77 |
await tx.payment.create({
|
|
@@ -85,8 +102,7 @@ export async function paymentRoutes(fastify: FastifyInstance) {
|
|
| 85 |
}
|
| 86 |
});
|
| 87 |
|
| 88 |
-
// 2. Create the Enrollment
|
| 89 |
-
// Check if an enrollment already exists to avoid duplicates
|
| 90 |
const existingEnrollment = await tx.enrollment.findFirst({
|
| 91 |
where: { userId, trackId }
|
| 92 |
});
|
|
@@ -100,18 +116,16 @@ export async function paymentRoutes(fastify: FastifyInstance) {
|
|
| 100 |
currentDay: 1
|
| 101 |
}
|
| 102 |
});
|
| 103 |
-
fastify.log.info(`[
|
| 104 |
}
|
| 105 |
});
|
| 106 |
} catch (dbError) {
|
| 107 |
-
fastify.log.error(dbError, '[
|
| 108 |
-
//
|
| 109 |
-
// but you'd want robust alerting here.
|
| 110 |
}
|
| 111 |
}
|
| 112 |
}
|
| 113 |
|
| 114 |
-
// Return a 200 response to acknowledge receipt of the event
|
| 115 |
reply.send({ received: true });
|
| 116 |
});
|
| 117 |
}
|
|
|
|
| 1 |
import { FastifyInstance } from 'fastify';
|
| 2 |
import { stripeService } from '../services/stripe';
|
| 3 |
+
import { prisma } from '../services/prisma';
|
| 4 |
+
import { z } from 'zod';
|
| 5 |
|
| 6 |
+
// ─── Shared Zod schemas ────────────────────────────────────────────────────────
|
| 7 |
+
const checkoutSchema = z.object({
|
| 8 |
+
userId: z.string().uuid(),
|
| 9 |
+
trackId: z.string().uuid(),
|
| 10 |
+
});
|
| 11 |
|
| 12 |
+
// ─── Private routes (require ADMIN_API_KEY) ───────────────────────────────────
|
| 13 |
export async function paymentRoutes(fastify: FastifyInstance) {
|
| 14 |
|
| 15 |
// Create a Checkout Session
|
| 16 |
fastify.post('/checkout', async (request, reply) => {
|
| 17 |
+
const parseResult = checkoutSchema.safeParse(request.body);
|
| 18 |
+
if (!parseResult.success) {
|
| 19 |
+
return reply.status(400).send({ error: 'Invalid request body', details: parseResult.error.flatten() });
|
|
|
|
| 20 |
}
|
| 21 |
|
| 22 |
+
const { userId, trackId } = parseResult.data;
|
| 23 |
+
|
| 24 |
try {
|
| 25 |
// Validate the track exists and is premium
|
| 26 |
const track = await prisma.track.findUnique({ where: { id: trackId } });
|
|
|
|
| 48 |
return reply.status(500).send({ error: 'Failed to create checkout session' });
|
| 49 |
}
|
| 50 |
});
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// ─── Public Stripe webhook (no API key auth — secured by Stripe signature) ────
|
| 54 |
+
export async function stripeWebhookRoute(fastify: FastifyInstance) {
|
| 55 |
+
// Capture raw body buffer for Stripe signature verification
|
| 56 |
+
fastify.addContentTypeParser('application/json', { parseAs: 'buffer' }, (req, body, done) => {
|
| 57 |
+
(req as any).rawBody = body;
|
| 58 |
+
try {
|
| 59 |
+
done(null, JSON.parse(body.toString('utf8')));
|
| 60 |
+
} catch (err: any) {
|
| 61 |
+
done(err as Error, undefined as unknown as Buffer);
|
| 62 |
+
}
|
| 63 |
+
});
|
| 64 |
|
| 65 |
+
fastify.post('/webhook', async (request, reply) => {
|
|
|
|
|
|
|
| 66 |
const sig = request.headers['stripe-signature'];
|
| 67 |
|
| 68 |
if (!sig || typeof sig !== 'string') {
|
|
|
|
| 72 |
let event;
|
| 73 |
|
| 74 |
try {
|
| 75 |
+
const rawBody = (request as any).rawBody as Buffer;
|
| 76 |
+
event = stripeService.verifyWebhookSignature(rawBody, sig);
|
| 77 |
} catch (err: any) {
|
| 78 |
+
fastify.log.warn(`[Stripe Webhook] Signature verification failed: ${err.message}`);
|
| 79 |
return reply.status(400).send(`Webhook Error: ${err.message}`);
|
| 80 |
}
|
| 81 |
|
|
|
|
| 89 |
|
| 90 |
if (userId && trackId) {
|
| 91 |
try {
|
|
|
|
| 92 |
await prisma.$transaction(async (tx: any) => {
|
| 93 |
// 1. Record the payment
|
| 94 |
await tx.payment.create({
|
|
|
|
| 102 |
}
|
| 103 |
});
|
| 104 |
|
| 105 |
+
// 2. Create the Enrollment (dedup)
|
|
|
|
| 106 |
const existingEnrollment = await tx.enrollment.findFirst({
|
| 107 |
where: { userId, trackId }
|
| 108 |
});
|
|
|
|
| 116 |
currentDay: 1
|
| 117 |
}
|
| 118 |
});
|
| 119 |
+
fastify.log.info(`[Stripe Webhook] Enrollment created for User ${userId}, Track ${trackId}`);
|
| 120 |
}
|
| 121 |
});
|
| 122 |
} catch (dbError) {
|
| 123 |
+
fastify.log.error(dbError, '[Stripe Webhook] Database error during webhook processing');
|
| 124 |
+
// Still return 200 so Stripe doesn't retry endlessly — but alert monitoring.
|
|
|
|
| 125 |
}
|
| 126 |
}
|
| 127 |
}
|
| 128 |
|
|
|
|
| 129 |
reply.send({ received: true });
|
| 130 |
});
|
| 131 |
}
|
apps/api/src/services/stripe.ts
CHANGED
|
@@ -6,9 +6,13 @@ export class StripeService {
|
|
| 6 |
private clientUrl: string;
|
| 7 |
|
| 8 |
constructor() {
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
this.clientUrl = process.env.VITE_CLIENT_URL || 'http://localhost:5174';
|
| 13 |
|
| 14 |
this.stripe = new Stripe(secretKey, {
|
|
|
|
| 6 |
private clientUrl: string;
|
| 7 |
|
| 8 |
constructor() {
|
| 9 |
+
const secretKey = process.env.STRIPE_SECRET_KEY;
|
| 10 |
+
if (!secretKey) throw new Error('[StripeService] STRIPE_SECRET_KEY is required');
|
| 11 |
+
|
| 12 |
+
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
|
| 13 |
+
if (!webhookSecret) throw new Error('[StripeService] STRIPE_WEBHOOK_SECRET is required');
|
| 14 |
+
|
| 15 |
+
this.webhookSecret = webhookSecret;
|
| 16 |
this.clientUrl = process.env.VITE_CLIENT_URL || 'http://localhost:5174';
|
| 17 |
|
| 18 |
this.stripe = new Stripe(secretKey, {
|
apps/web/src/App.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
import { BrowserRouter as Router, Routes, Route, Link, useNavigate } from 'react-router-dom';
|
| 2 |
import { BookOpen, FileText, Smartphone, ArrowRight, Phone } from 'lucide-react';
|
| 3 |
|
|
|
|
|
|
|
| 4 |
function Navbar() {
|
| 5 |
return (
|
| 6 |
<nav className="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-100">
|
|
@@ -17,7 +19,7 @@ function Navbar() {
|
|
| 17 |
Student Login
|
| 18 |
</Link>
|
| 19 |
<a
|
| 20 |
-
href=
|
| 21 |
target="_blank"
|
| 22 |
rel="noreferrer"
|
| 23 |
className="bg-primary text-white px-5 py-2.5 rounded-full font-medium hover:bg-emerald-700 hover:shadow-lg hover:shadow-primary/30 transition-all active:scale-95 hidden sm:inline-flex items-center"
|
|
@@ -60,7 +62,7 @@ function Hero() {
|
|
| 60 |
|
| 61 |
<div className="flex flex-col sm:flex-row justify-center items-center gap-4">
|
| 62 |
<a
|
| 63 |
-
href=
|
| 64 |
target="_blank"
|
| 65 |
rel="noreferrer"
|
| 66 |
className="w-full sm:w-auto bg-primary text-white text-lg px-8 py-4 rounded-full font-bold shadow-xl shadow-primary/30 hover:bg-emerald-700 hover:scale-105 transition-all flex items-center justify-center group"
|
|
|
|
| 1 |
import { BrowserRouter as Router, Routes, Route, Link, useNavigate } from 'react-router-dom';
|
| 2 |
import { BookOpen, FileText, Smartphone, ArrowRight, Phone } from 'lucide-react';
|
| 3 |
|
| 4 |
+
const WA_NUMBER = import.meta.env.VITE_WHATSAPP_NUMBER || '221771234567';
|
| 5 |
+
|
| 6 |
function Navbar() {
|
| 7 |
return (
|
| 8 |
<nav className="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-100">
|
|
|
|
| 19 |
Student Login
|
| 20 |
</Link>
|
| 21 |
<a
|
| 22 |
+
href={`https://wa.me/${WA_NUMBER}?text=Hello! I want to enroll.`}
|
| 23 |
target="_blank"
|
| 24 |
rel="noreferrer"
|
| 25 |
className="bg-primary text-white px-5 py-2.5 rounded-full font-medium hover:bg-emerald-700 hover:shadow-lg hover:shadow-primary/30 transition-all active:scale-95 hidden sm:inline-flex items-center"
|
|
|
|
| 62 |
|
| 63 |
<div className="flex flex-col sm:flex-row justify-center items-center gap-4">
|
| 64 |
<a
|
| 65 |
+
href={`https://wa.me/${WA_NUMBER}?text=Start`}
|
| 66 |
target="_blank"
|
| 67 |
rel="noreferrer"
|
| 68 |
className="w-full sm:w-auto bg-primary text-white text-lg px-8 py-4 rounded-full font-bold shadow-xl shadow-primary/30 hover:bg-emerald-700 hover:scale-105 transition-all flex items-center justify-center group"
|
apps/web/src/vite-env.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="vite/client" />
|
| 2 |
+
|
| 3 |
+
interface ImportMetaEnv {
|
| 4 |
+
readonly VITE_API_URL: string;
|
| 5 |
+
readonly VITE_CLIENT_URL: string;
|
| 6 |
+
readonly VITE_WHATSAPP_NUMBER: string;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
interface ImportMeta {
|
| 10 |
+
readonly env: ImportMetaEnv;
|
| 11 |
+
}
|
apps/whatsapp-worker/src/index.ts
CHANGED
|
@@ -40,10 +40,14 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
|
|
| 40 |
if (track.isPremium) {
|
| 41 |
console.log(`[WORKER] User ${userId} requested Premium Track ${trackId}. Generating Payment Link...`);
|
| 42 |
try {
|
| 43 |
-
const API_URL = process.env.
|
|
|
|
| 44 |
const checkoutRes = await fetch(`${API_URL}/v1/payments/checkout`, {
|
| 45 |
method: 'POST',
|
| 46 |
-
headers: {
|
|
|
|
|
|
|
|
|
|
| 47 |
body: JSON.stringify({ userId, trackId })
|
| 48 |
});
|
| 49 |
|
|
@@ -87,10 +91,14 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
|
|
| 87 |
if (user && user.activity) {
|
| 88 |
try {
|
| 89 |
console.log(`[WORKER] Personalizing lesson for User ${userId}'s activity: ${user.activity}`);
|
| 90 |
-
const API_URL = process.env.
|
|
|
|
| 91 |
const personalizeRes = await fetch(`${API_URL}/v1/ai/personalize-lesson`, {
|
| 92 |
method: 'POST',
|
| 93 |
-
headers: {
|
|
|
|
|
|
|
|
|
|
| 94 |
body: JSON.stringify({ lessonText: trackDay.textContent, userActivity: user.activity })
|
| 95 |
});
|
| 96 |
|
|
@@ -109,10 +117,14 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
|
|
| 109 |
let audioUrl = null;
|
| 110 |
try {
|
| 111 |
console.log(`[WORKER] Generating TTS Audio for User ${userId}...`);
|
| 112 |
-
const API_URL = process.env.
|
|
|
|
| 113 |
const ttsRes = await fetch(`${API_URL}/v1/ai/tts`, {
|
| 114 |
method: 'POST',
|
| 115 |
-
headers: {
|
|
|
|
|
|
|
|
|
|
| 116 |
body: JSON.stringify({ text: finalContent })
|
| 117 |
});
|
| 118 |
|
|
@@ -154,12 +166,17 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
|
|
| 154 |
// Update userContext to explicitly reference the user's sector/activity
|
| 155 |
const userContext = `User ${userId} completed the Business Pitch track. Their business activity/sector is: ${user?.activity || 'Unknown'}. They want to build a business in this sector using the concepts learned in the track.`;
|
| 156 |
|
| 157 |
-
const API_URL = process.env.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
// Trigger OnePager (PDF)
|
| 160 |
const pdfRes = await fetch(`${API_URL}/v1/ai/onepager`, {
|
| 161 |
method: 'POST',
|
| 162 |
-
headers:
|
| 163 |
body: JSON.stringify({ userContext })
|
| 164 |
});
|
| 165 |
const pdfData = await pdfRes.json();
|
|
@@ -167,7 +184,7 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
|
|
| 167 |
// Trigger Pitch Deck (PPTX)
|
| 168 |
const pptxRes = await fetch(`${API_URL}/v1/ai/deck`, {
|
| 169 |
method: 'POST',
|
| 170 |
-
headers:
|
| 171 |
body: JSON.stringify({ userContext })
|
| 172 |
});
|
| 173 |
const pptxData = await pptxRes.json();
|
|
|
|
| 40 |
if (track.isPremium) {
|
| 41 |
console.log(`[WORKER] User ${userId} requested Premium Track ${trackId}. Generating Payment Link...`);
|
| 42 |
try {
|
| 43 |
+
const API_URL = process.env.API_URL || 'http://localhost:3001';
|
| 44 |
+
const apiKey = process.env.ADMIN_API_KEY || '';
|
| 45 |
const checkoutRes = await fetch(`${API_URL}/v1/payments/checkout`, {
|
| 46 |
method: 'POST',
|
| 47 |
+
headers: {
|
| 48 |
+
'Content-Type': 'application/json',
|
| 49 |
+
'Authorization': `Bearer ${apiKey}`
|
| 50 |
+
},
|
| 51 |
body: JSON.stringify({ userId, trackId })
|
| 52 |
});
|
| 53 |
|
|
|
|
| 91 |
if (user && user.activity) {
|
| 92 |
try {
|
| 93 |
console.log(`[WORKER] Personalizing lesson for User ${userId}'s activity: ${user.activity}`);
|
| 94 |
+
const API_URL = process.env.API_URL || 'http://localhost:3001';
|
| 95 |
+
const apiKey = process.env.ADMIN_API_KEY || '';
|
| 96 |
const personalizeRes = await fetch(`${API_URL}/v1/ai/personalize-lesson`, {
|
| 97 |
method: 'POST',
|
| 98 |
+
headers: {
|
| 99 |
+
'Content-Type': 'application/json',
|
| 100 |
+
'Authorization': `Bearer ${apiKey}`
|
| 101 |
+
},
|
| 102 |
body: JSON.stringify({ lessonText: trackDay.textContent, userActivity: user.activity })
|
| 103 |
});
|
| 104 |
|
|
|
|
| 117 |
let audioUrl = null;
|
| 118 |
try {
|
| 119 |
console.log(`[WORKER] Generating TTS Audio for User ${userId}...`);
|
| 120 |
+
const API_URL = process.env.API_URL || 'http://localhost:3001';
|
| 121 |
+
const apiKey = process.env.ADMIN_API_KEY || '';
|
| 122 |
const ttsRes = await fetch(`${API_URL}/v1/ai/tts`, {
|
| 123 |
method: 'POST',
|
| 124 |
+
headers: {
|
| 125 |
+
'Content-Type': 'application/json',
|
| 126 |
+
'Authorization': `Bearer ${apiKey}`
|
| 127 |
+
},
|
| 128 |
body: JSON.stringify({ text: finalContent })
|
| 129 |
});
|
| 130 |
|
|
|
|
| 166 |
// Update userContext to explicitly reference the user's sector/activity
|
| 167 |
const userContext = `User ${userId} completed the Business Pitch track. Their business activity/sector is: ${user?.activity || 'Unknown'}. They want to build a business in this sector using the concepts learned in the track.`;
|
| 168 |
|
| 169 |
+
const API_URL = process.env.API_URL || 'http://localhost:3001';
|
| 170 |
+
const apiKey = process.env.ADMIN_API_KEY || '';
|
| 171 |
+
const authHeaders = {
|
| 172 |
+
'Content-Type': 'application/json',
|
| 173 |
+
'Authorization': `Bearer ${apiKey}`
|
| 174 |
+
};
|
| 175 |
|
| 176 |
// Trigger OnePager (PDF)
|
| 177 |
const pdfRes = await fetch(`${API_URL}/v1/ai/onepager`, {
|
| 178 |
method: 'POST',
|
| 179 |
+
headers: authHeaders,
|
| 180 |
body: JSON.stringify({ userContext })
|
| 181 |
});
|
| 182 |
const pdfData = await pdfRes.json();
|
|
|
|
| 184 |
// Trigger Pitch Deck (PPTX)
|
| 185 |
const pptxRes = await fetch(`${API_URL}/v1/ai/deck`, {
|
| 186 |
method: 'POST',
|
| 187 |
+
headers: authHeaders,
|
| 188 |
body: JSON.stringify({ userContext })
|
| 189 |
});
|
| 190 |
const pptxData = await pptxRes.json();
|