- Add ioredis dependency to API package - Create Redis connection utility (apps/api/src/config/redis.ts) - Create Redis-backed spam rate limit middleware with per-minute and daily limits - Create spam classification routes (SMS, number reputation, call analysis, feedback) - Register middleware and routes in API server - Add 7 passing tests for rate limit enforcement - Update vitest config with required env vars Co-Authored-By: Paperclip <noreply@paperclip.ing>
202 lines
6.1 KiB
TypeScript
202 lines
6.1 KiB
TypeScript
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
import {
|
|
numberReputationService,
|
|
smsClassifierService,
|
|
callAnalysisService,
|
|
spamFeedbackService,
|
|
} from '../services/spamshield';
|
|
|
|
export async function spamshieldRoutes(fastify: FastifyInstance) {
|
|
// Classify SMS text
|
|
fastify.post('/sms/classify', async (request: FastifyRequest, reply: FastifyReply) => {
|
|
const authReq = request as FastifyRequest & { user?: { id: string } };
|
|
const userId = authReq.user?.id;
|
|
|
|
if (!userId) {
|
|
return reply.code(401).send({ error: 'User ID required' });
|
|
}
|
|
|
|
const body = request.body as { text: string };
|
|
|
|
if (!body.text || typeof body.text !== 'string') {
|
|
return reply.code(400).send({ error: 'text is required' });
|
|
}
|
|
|
|
try {
|
|
const result = await smsClassifierService.classify(body.text);
|
|
return reply.send({
|
|
classification: {
|
|
isSpam: result.isSpam,
|
|
confidence: result.confidence,
|
|
spamFeatures: result.spamFeatures,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Classification failed';
|
|
return reply.code(422).send({ error: message });
|
|
}
|
|
});
|
|
|
|
// Check number reputation
|
|
fastify.post('/number/reputation', async (request: FastifyRequest, reply: FastifyReply) => {
|
|
const authReq = request as FastifyRequest & { user?: { id: string } };
|
|
const userId = authReq.user?.id;
|
|
|
|
if (!userId) {
|
|
return reply.code(401).send({ error: 'User ID required' });
|
|
}
|
|
|
|
const body = request.body as { phoneNumber: string };
|
|
|
|
if (!body.phoneNumber || typeof body.phoneNumber !== 'string') {
|
|
return reply.code(400).send({ error: 'phoneNumber is required' });
|
|
}
|
|
|
|
try {
|
|
const result = await numberReputationService.checkReputation(body.phoneNumber);
|
|
return reply.send({
|
|
reputation: {
|
|
isSpam: result.isSpam,
|
|
confidence: result.confidence,
|
|
spamType: result.spamType,
|
|
reportCount: result.reportCount,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Reputation check failed';
|
|
return reply.code(422).send({ error: message });
|
|
}
|
|
});
|
|
|
|
// Analyze incoming call
|
|
fastify.post('/call/analyze', async (request: FastifyRequest, reply: FastifyReply) => {
|
|
const authReq = request as FastifyRequest & { user?: { id: string } };
|
|
const userId = authReq.user?.id;
|
|
|
|
if (!userId) {
|
|
return reply.code(401).send({ error: 'User ID required' });
|
|
}
|
|
|
|
const body = request.body as {
|
|
phoneNumber: string;
|
|
duration?: number;
|
|
callTime: string;
|
|
isVoip?: boolean;
|
|
};
|
|
|
|
if (!body.phoneNumber || !body.callTime) {
|
|
return reply.code(400).send({ error: 'phoneNumber and callTime are required' });
|
|
}
|
|
|
|
try {
|
|
const result = await callAnalysisService.analyzeCall({
|
|
phoneNumber: body.phoneNumber,
|
|
duration: body.duration,
|
|
callTime: new Date(body.callTime),
|
|
isVoip: body.isVoip,
|
|
});
|
|
return reply.send({
|
|
analysis: {
|
|
decision: result.decision,
|
|
confidence: result.confidence,
|
|
reasons: result.reasons,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Call analysis failed';
|
|
return reply.code(422).send({ error: message });
|
|
}
|
|
});
|
|
|
|
// Record spam feedback
|
|
fastify.post('/feedback', async (request: FastifyRequest, reply: FastifyReply) => {
|
|
const authReq = request as FastifyRequest & { user?: { id: string } };
|
|
const userId = authReq.user?.id;
|
|
|
|
if (!userId) {
|
|
return reply.code(401).send({ error: 'User ID required' });
|
|
}
|
|
|
|
const body = request.body as {
|
|
phoneNumber: string;
|
|
isSpam: boolean;
|
|
confidence?: number;
|
|
metadata?: Record<string, unknown>;
|
|
};
|
|
|
|
if (!body.phoneNumber || typeof body.isSpam !== 'boolean') {
|
|
return reply.code(400).send({ error: 'phoneNumber and isSpam are required' });
|
|
}
|
|
|
|
try {
|
|
const feedback = await spamFeedbackService.recordFeedback(
|
|
userId,
|
|
body.phoneNumber,
|
|
body.isSpam,
|
|
body.confidence,
|
|
body.metadata
|
|
);
|
|
return reply.code(201).send({
|
|
feedback: {
|
|
id: feedback.id,
|
|
phoneNumber: feedback.phoneNumber,
|
|
isSpam: feedback.isSpam,
|
|
createdAt: feedback.createdAt,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Feedback recording failed';
|
|
return reply.code(422).send({ error: message });
|
|
}
|
|
});
|
|
|
|
// Get spam history
|
|
fastify.get('/history', async (request: FastifyRequest, reply: FastifyReply) => {
|
|
const authReq = request as FastifyRequest & { user?: { id: string } };
|
|
const userId = authReq.user?.id;
|
|
|
|
if (!userId) {
|
|
return reply.code(401).send({ error: 'User ID required' });
|
|
}
|
|
|
|
const query = request.query as {
|
|
limit?: string;
|
|
isSpam?: string;
|
|
startDate?: string;
|
|
};
|
|
|
|
const results = await spamFeedbackService.getSpamHistory(userId, {
|
|
limit: query.limit ? parseInt(query.limit, 10) : undefined,
|
|
isSpam: query.isSpam !== undefined ? query.isSpam === 'true' : undefined,
|
|
startDate: query.startDate ? new Date(query.startDate) : undefined,
|
|
});
|
|
|
|
return reply.send({
|
|
history: results.map((r) => ({
|
|
id: r.id,
|
|
phoneNumber: r.phoneNumber,
|
|
isSpam: r.isSpam,
|
|
createdAt: r.createdAt,
|
|
})),
|
|
});
|
|
});
|
|
|
|
// Get spam statistics
|
|
fastify.get('/statistics', async (request: FastifyRequest, reply: FastifyReply) => {
|
|
const authReq = request as FastifyRequest & { user?: { id: string } };
|
|
const userId = authReq.user?.id;
|
|
|
|
if (!userId) {
|
|
return reply.code(401).send({ error: 'User ID required' });
|
|
}
|
|
|
|
try {
|
|
const stats = await spamFeedbackService.getStatistics(userId);
|
|
return reply.send({ statistics: stats });
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Statistics retrieval failed';
|
|
return reply.code(422).send({ error: message });
|
|
}
|
|
});
|
|
}
|