CognxSafeTrack Claude Sonnet 4.6 commited on
Commit ·
aa4f69f
1
Parent(s): 2859b85
fix: security hardening, log all silent catch blocks, remove prisma cast
Browse filesSecurity:
- whatsapp.ts: remove WHATSAPP_VERIFY_TOKEN hardcoded fallback — return 500
if env var is missing instead of silently accepting a public default
- rateLimit.ts: key by req.ip (not spoofable x-organization-id header)
Code quality — 7 empty catch blocks now log with context:
- NudgeHandler, AdminHandler (+ missing logger import), EnrollHandler (x2),
MediaHandler (x2), organization.ts invalidateOrgCache
- admin.ts: remove (prisma as any).normalizationRule — model is in schema,
cast was unnecessary
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- apps/api/src/middleware/rateLimit.ts +1 -1
- apps/api/src/routes/admin.ts +1 -1
- apps/api/src/routes/whatsapp.ts +2 -1
- apps/whatsapp-worker/src/handlers/AdminHandler.ts +4 -1
- apps/whatsapp-worker/src/handlers/EnrollHandler.ts +7 -2
- apps/whatsapp-worker/src/handlers/MediaHandler.ts +6 -2
- apps/whatsapp-worker/src/handlers/NudgeHandler.ts +3 -1
- apps/whatsapp-worker/src/services/organization.ts +3 -1
apps/api/src/middleware/rateLimit.ts
CHANGED
|
@@ -7,7 +7,7 @@ export async function setupRateLimit(server: FastifyInstance) {
|
|
| 7 |
await server.register(rateLimit as any, {
|
| 8 |
max: 100,
|
| 9 |
timeWindow: '1 minute',
|
| 10 |
-
keyGenerator: (req: FastifyRequest) =>
|
| 11 |
errorResponseBuilder: (_request: FastifyRequest, context: { after: string }) => {
|
| 12 |
return {
|
| 13 |
statusCode: 429,
|
|
|
|
| 7 |
await server.register(rateLimit as any, {
|
| 8 |
max: 100,
|
| 9 |
timeWindow: '1 minute',
|
| 10 |
+
keyGenerator: (req: FastifyRequest) => req.ip as string,
|
| 11 |
errorResponseBuilder: (_request: FastifyRequest, context: { after: string }) => {
|
| 12 |
return {
|
| 13 |
statusCode: 429,
|
apps/api/src/routes/admin.ts
CHANGED
|
@@ -448,7 +448,7 @@ export async function adminRoutes(fastify: FastifyInstance) {
|
|
| 448 |
|
| 449 |
// List current normalization rules from DB
|
| 450 |
fastify.get('/training/rules', async () => {
|
| 451 |
-
return
|
| 452 |
orderBy: { original: 'asc' }
|
| 453 |
});
|
| 454 |
});
|
|
|
|
| 448 |
|
| 449 |
// List current normalization rules from DB
|
| 450 |
fastify.get('/training/rules', async () => {
|
| 451 |
+
return prisma.normalizationRule.findMany({
|
| 452 |
orderBy: { original: 'asc' }
|
| 453 |
});
|
| 454 |
});
|
apps/api/src/routes/whatsapp.ts
CHANGED
|
@@ -45,7 +45,8 @@ export async function whatsappRoutes(fastify: FastifyInstance) {
|
|
| 45 |
const token = query['hub.verify_token'];
|
| 46 |
const challenge = query['hub.challenge'];
|
| 47 |
|
| 48 |
-
const VERIFY_TOKEN = process.env.WHATSAPP_VERIFY_TOKEN
|
|
|
|
| 49 |
|
| 50 |
if (mode && token) {
|
| 51 |
if (mode === 'subscribe' && token === VERIFY_TOKEN) {
|
|
|
|
| 45 |
const token = query['hub.verify_token'];
|
| 46 |
const challenge = query['hub.challenge'];
|
| 47 |
|
| 48 |
+
const VERIFY_TOKEN = process.env.WHATSAPP_VERIFY_TOKEN;
|
| 49 |
+
if (!VERIFY_TOKEN) return reply.code(500).send({ error: 'WHATSAPP_VERIFY_TOKEN not configured' });
|
| 50 |
|
| 51 |
if (mode && token) {
|
| 52 |
if (mode === 'subscribe' && token === VERIFY_TOKEN) {
|
apps/whatsapp-worker/src/handlers/AdminHandler.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Job, Queue } from 'bullmq';
|
|
| 2 |
import Redis from 'ioredis';
|
| 3 |
import { JobHandler, JobData } from './types';
|
| 4 |
import { prisma } from '../services/prisma';
|
|
|
|
| 5 |
import { sendTextMessage } from '../whatsapp-cloud';
|
| 6 |
|
| 7 |
interface TenantConfig {
|
|
@@ -15,7 +16,9 @@ export class AdminHandler implements JobHandler {
|
|
| 15 |
try {
|
| 16 |
const cached = await connection.get(cacheKey);
|
| 17 |
if (cached) return JSON.parse(cached);
|
| 18 |
-
} catch (err) {
|
|
|
|
|
|
|
| 19 |
|
| 20 |
const org = await prisma.organization.findUnique({
|
| 21 |
where: { id: organizationId },
|
|
|
|
| 2 |
import Redis from 'ioredis';
|
| 3 |
import { JobHandler, JobData } from './types';
|
| 4 |
import { prisma } from '../services/prisma';
|
| 5 |
+
import { logger } from '../logger';
|
| 6 |
import { sendTextMessage } from '../whatsapp-cloud';
|
| 7 |
|
| 8 |
interface TenantConfig {
|
|
|
|
| 16 |
try {
|
| 17 |
const cached = await connection.get(cacheKey);
|
| 18 |
if (cached) return JSON.parse(cached);
|
| 19 |
+
} catch (err) {
|
| 20 |
+
logger.warn({ err, organizationId }, '[AdminHandler] Redis cache lookup failed');
|
| 21 |
+
}
|
| 22 |
|
| 23 |
const org = await prisma.organization.findUnique({
|
| 24 |
where: { id: organizationId },
|
apps/whatsapp-worker/src/handlers/EnrollHandler.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Job, Queue } from 'bullmq';
|
|
| 2 |
import Redis from 'ioredis';
|
| 3 |
import { JobHandler, JobData } from './types';
|
| 4 |
import { prisma } from '../services/prisma';
|
|
|
|
| 5 |
import { sendTextMessage } from '../whatsapp-cloud';
|
| 6 |
import { getApiUrl, getAdminApiKey } from '../config';
|
| 7 |
|
|
@@ -16,7 +17,9 @@ export class EnrollHandler implements JobHandler {
|
|
| 16 |
try {
|
| 17 |
const cached = await connection.get(cacheKey);
|
| 18 |
if (cached) return JSON.parse(cached);
|
| 19 |
-
} catch (err) {
|
|
|
|
|
|
|
| 20 |
|
| 21 |
const org = await prisma.organization.findUnique({
|
| 22 |
where: { id: organizationId },
|
|
@@ -60,7 +63,9 @@ export class EnrollHandler implements JobHandler {
|
|
| 60 |
await sendTextMessage(user.phone, `💳 Cette formation est Premium. Complétez votre paiement ici :\n${checkoutData.url}`, tenantConfig);
|
| 61 |
}
|
| 62 |
}
|
| 63 |
-
} catch (err) {
|
|
|
|
|
|
|
| 64 |
} else {
|
| 65 |
const existing = await prisma.enrollment.findFirst({ where: { userId, trackId } });
|
| 66 |
if (!existing) {
|
|
|
|
| 2 |
import Redis from 'ioredis';
|
| 3 |
import { JobHandler, JobData } from './types';
|
| 4 |
import { prisma } from '../services/prisma';
|
| 5 |
+
import { logger } from '../logger';
|
| 6 |
import { sendTextMessage } from '../whatsapp-cloud';
|
| 7 |
import { getApiUrl, getAdminApiKey } from '../config';
|
| 8 |
|
|
|
|
| 17 |
try {
|
| 18 |
const cached = await connection.get(cacheKey);
|
| 19 |
if (cached) return JSON.parse(cached);
|
| 20 |
+
} catch (err) {
|
| 21 |
+
logger.warn({ err, organizationId }, '[EnrollHandler] Redis cache lookup failed');
|
| 22 |
+
}
|
| 23 |
|
| 24 |
const org = await prisma.organization.findUnique({
|
| 25 |
where: { id: organizationId },
|
|
|
|
| 63 |
await sendTextMessage(user.phone, `💳 Cette formation est Premium. Complétez votre paiement ici :\n${checkoutData.url}`, tenantConfig);
|
| 64 |
}
|
| 65 |
}
|
| 66 |
+
} catch (err) {
|
| 67 |
+
logger.error({ err, userId, trackId }, '[EnrollHandler] Premium checkout failed');
|
| 68 |
+
}
|
| 69 |
} else {
|
| 70 |
const existing = await prisma.enrollment.findFirst({ where: { userId, trackId } });
|
| 71 |
if (!existing) {
|
apps/whatsapp-worker/src/handlers/MediaHandler.ts
CHANGED
|
@@ -20,7 +20,9 @@ export class MediaHandler implements JobHandler {
|
|
| 20 |
try {
|
| 21 |
const cached = await connection.get(cacheKey);
|
| 22 |
if (cached) return JSON.parse(cached);
|
| 23 |
-
} catch (err) {
|
|
|
|
|
|
|
| 24 |
|
| 25 |
const org = await prisma.organization.findUnique({
|
| 26 |
where: { id: organizationId },
|
|
@@ -68,7 +70,9 @@ export class MediaHandler implements JobHandler {
|
|
| 68 |
headers: { 'x-organization-id': organizationId as string }
|
| 69 |
});
|
| 70 |
audioUrl = storeRes.data.url;
|
| 71 |
-
} catch (err) {
|
|
|
|
|
|
|
| 72 |
|
| 73 |
const user = await prisma.user.findFirst({ where: { phone } });
|
| 74 |
if (user) {
|
|
|
|
| 20 |
try {
|
| 21 |
const cached = await connection.get(cacheKey);
|
| 22 |
if (cached) return JSON.parse(cached);
|
| 23 |
+
} catch (err) {
|
| 24 |
+
logger.warn({ err, organizationId }, '[MediaHandler] Redis cache lookup failed');
|
| 25 |
+
}
|
| 26 |
|
| 27 |
const org = await prisma.organization.findUnique({
|
| 28 |
where: { id: organizationId },
|
|
|
|
| 70 |
headers: { 'x-organization-id': organizationId as string }
|
| 71 |
});
|
| 72 |
audioUrl = storeRes.data.url;
|
| 73 |
+
} catch (err) {
|
| 74 |
+
logger.warn({ err, mediaId }, '[MediaHandler] Failed to store audio to R2');
|
| 75 |
+
}
|
| 76 |
|
| 77 |
const user = await prisma.user.findFirst({ where: { phone } });
|
| 78 |
if (user) {
|
apps/whatsapp-worker/src/handlers/NudgeHandler.ts
CHANGED
|
@@ -16,7 +16,9 @@ export class NudgeHandler implements JobHandler {
|
|
| 16 |
try {
|
| 17 |
const cached = await connection.get(cacheKey);
|
| 18 |
if (cached) return JSON.parse(cached);
|
| 19 |
-
} catch (err) {
|
|
|
|
|
|
|
| 20 |
|
| 21 |
const org = await prisma.organization.findUnique({
|
| 22 |
where: { id: organizationId },
|
|
|
|
| 16 |
try {
|
| 17 |
const cached = await connection.get(cacheKey);
|
| 18 |
if (cached) return JSON.parse(cached);
|
| 19 |
+
} catch (err) {
|
| 20 |
+
logger.warn({ err, organizationId }, '[NudgeHandler] Redis cache lookup failed');
|
| 21 |
+
}
|
| 22 |
|
| 23 |
const org = await prisma.organization.findUnique({
|
| 24 |
where: { id: organizationId },
|
apps/whatsapp-worker/src/services/organization.ts
CHANGED
|
@@ -82,7 +82,9 @@ export async function invalidateOrgCache(id: string) {
|
|
| 82 |
try {
|
| 83 |
localCache.delete(`org:full:${id}`);
|
| 84 |
await redis.del(`org:full:${id}`);
|
| 85 |
-
} catch (err) {
|
|
|
|
|
|
|
| 86 |
}
|
| 87 |
|
| 88 |
export async function getOrganizationByPhoneNumberId(phoneNumberId: string): Promise<string> {
|
|
|
|
| 82 |
try {
|
| 83 |
localCache.delete(`org:full:${id}`);
|
| 84 |
await redis.del(`org:full:${id}`);
|
| 85 |
+
} catch (err) {
|
| 86 |
+
logger.warn({ err, id }, '[OrgCache] Cache invalidation failed');
|
| 87 |
+
}
|
| 88 |
}
|
| 89 |
|
| 90 |
export async function getOrganizationByPhoneNumberId(phoneNumberId: string): Promise<string> {
|