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 files

Security:
- 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 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) => (req.headers['x-organization-id'] as string) || req.ip,
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 (prisma as any).normalizationRule.findMany({
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 || 'xamle_studio_secret_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> {