more package declarations
This commit is contained in:
@@ -1,4 +1,11 @@
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || process.env.NEXTAUTH_SECRET;
|
||||
|
||||
if (!JWT_SECRET && process.env.NODE_ENV === 'production') {
|
||||
console.error('JWT_SECRET or NEXTAUTH_SECRET must be set in production');
|
||||
}
|
||||
|
||||
export interface AuthRequest extends FastifyRequest {
|
||||
user?: {
|
||||
@@ -27,17 +34,35 @@ export async function authMiddleware(fastify: FastifyInstance) {
|
||||
if (authHeader?.startsWith('Bearer ')) {
|
||||
const token = authHeader.slice(7);
|
||||
try {
|
||||
// In production, decode and verify JWT
|
||||
// For now, we'll attach a placeholder user
|
||||
if (!JWT_SECRET) {
|
||||
throw new Error('JWT_SECRET not configured');
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as {
|
||||
id: string;
|
||||
email: string;
|
||||
role: string;
|
||||
organizationId?: string;
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
};
|
||||
|
||||
authReq.user = {
|
||||
id: 'user-placeholder',
|
||||
email: 'user@example.com',
|
||||
role: 'user',
|
||||
id: decoded.id,
|
||||
email: decoded.email,
|
||||
role: decoded.role,
|
||||
organizationId: decoded.organizationId,
|
||||
};
|
||||
authReq.authType = 'jwt';
|
||||
return;
|
||||
} catch (err) {
|
||||
// JWT invalid, continue to API key check
|
||||
if (err instanceof jwt.JsonWebTokenError) {
|
||||
throw { statusCode: 401, message: 'Invalid token' };
|
||||
}
|
||||
if (err instanceof jwt.TokenExpiredError) {
|
||||
throw { statusCode: 401, message: 'Token expired', expiredAt: err.expiredAt };
|
||||
}
|
||||
throw { statusCode: 401, message: 'Authentication failed' };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,39 +7,97 @@ function getUserId(request: FastifyRequest): string | undefined {
|
||||
return (request.user as AuthUser | undefined)?.id;
|
||||
}
|
||||
|
||||
const timeWindowSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
timeWindow: { type: "integer", minimum: 1, maximum: 10080 },
|
||||
},
|
||||
};
|
||||
|
||||
const paginatedQuerySchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
timeWindow: { type: "integer", minimum: 1, maximum: 10080 },
|
||||
limit: { type: "integer", minimum: 1, maximum: 200 },
|
||||
offset: { type: "integer", minimum: 0, maximum: 10000 },
|
||||
},
|
||||
};
|
||||
|
||||
export function correlationRoutes(fastify: FastifyInstance) {
|
||||
fastify.get("/dashboard", async (request, reply) => {
|
||||
const userId = getUserId(request);
|
||||
if (!userId || userId === "anonymous") {
|
||||
return reply.code(401).send({ error: "User not authenticated" });
|
||||
fastify.get(
|
||||
"/dashboard",
|
||||
{
|
||||
schema: {
|
||||
...timeWindowSchema,
|
||||
response: {
|
||||
"400": {
|
||||
type: "object",
|
||||
properties: {
|
||||
error: { type: "string" },
|
||||
},
|
||||
},
|
||||
"401": {
|
||||
type: "object",
|
||||
properties: {
|
||||
error: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const userId = getUserId(request);
|
||||
if (!userId || userId === "anonymous") {
|
||||
return reply.code(401).send({ error: "User not authenticated" });
|
||||
}
|
||||
|
||||
const query = request.query as Record<string, string | number>;
|
||||
const timeWindow =
|
||||
typeof query.timeWindow === "number" ? query.timeWindow : 60;
|
||||
const data = await correlationService.getDashboardData(userId, timeWindow);
|
||||
return reply.send(data);
|
||||
}
|
||||
);
|
||||
|
||||
const timeWindow =
|
||||
parseInt(
|
||||
(request.query as Record<string, string>).timeWindow as string
|
||||
) || 60;
|
||||
const data = await correlationService.getDashboardData(userId, timeWindow);
|
||||
return reply.send(data);
|
||||
});
|
||||
fastify.get(
|
||||
"/groups",
|
||||
{
|
||||
schema: {
|
||||
...paginatedQuerySchema,
|
||||
response: {
|
||||
"400": {
|
||||
type: "object",
|
||||
properties: {
|
||||
error: { type: "string" },
|
||||
},
|
||||
},
|
||||
"401": {
|
||||
type: "object",
|
||||
properties: {
|
||||
error: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const userId = getUserId(request);
|
||||
if (!userId || userId === "anonymous") {
|
||||
return reply.code(401).send({ error: "User not authenticated" });
|
||||
}
|
||||
|
||||
fastify.get("/groups", async (request, reply) => {
|
||||
const userId = getUserId(request);
|
||||
if (!userId || userId === "anonymous") {
|
||||
return reply.code(401).send({ error: "User not authenticated" });
|
||||
const query = request.query as Record<string, string | number>;
|
||||
const result = await correlationService.getCorrelationGroups({
|
||||
userId,
|
||||
status: (query.status as any) || undefined,
|
||||
timeWindowMinutes:
|
||||
typeof query.timeWindow === "number" ? query.timeWindow : 60,
|
||||
limit: typeof query.limit === "number" ? query.limit : 50,
|
||||
offset: typeof query.offset === "number" ? query.offset : 0,
|
||||
});
|
||||
return reply.send(result);
|
||||
}
|
||||
|
||||
const query = request.query as Record<string, string>;
|
||||
const result = await correlationService.getCorrelationGroups({
|
||||
userId,
|
||||
status: (query.status as any) || undefined,
|
||||
timeWindowMinutes: query.timeWindow
|
||||
? parseInt(query.timeWindow)
|
||||
: 60,
|
||||
limit: query.limit ? parseInt(query.limit) : 50,
|
||||
offset: query.offset ? parseInt(query.offset) : 0,
|
||||
});
|
||||
return reply.send(result);
|
||||
});
|
||||
);
|
||||
|
||||
fastify.get(
|
||||
"/groups/:groupId",
|
||||
@@ -114,26 +172,47 @@ export function correlationRoutes(fastify: FastifyInstance) {
|
||||
}
|
||||
);
|
||||
|
||||
fastify.get("/alerts", async (request, reply) => {
|
||||
const userId = getUserId(request);
|
||||
if (!userId || userId === "anonymous") {
|
||||
return reply.code(401).send({ error: "User not authenticated" });
|
||||
}
|
||||
fastify.get(
|
||||
"/alerts",
|
||||
{
|
||||
schema: {
|
||||
...paginatedQuerySchema,
|
||||
response: {
|
||||
"400": {
|
||||
type: "object",
|
||||
properties: {
|
||||
error: { type: "string" },
|
||||
},
|
||||
},
|
||||
"401": {
|
||||
type: "object",
|
||||
properties: {
|
||||
error: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const userId = getUserId(request);
|
||||
if (!userId || userId === "anonymous") {
|
||||
return reply.code(401).send({ error: "User not authenticated" });
|
||||
}
|
||||
|
||||
const query = request.query as Record<string, string>;
|
||||
const result = await correlationService.getCorrelatedAlerts({
|
||||
userId,
|
||||
source: (query.source as any) || undefined,
|
||||
category: (query.category as any) || undefined,
|
||||
severity: (query.severity as any) || undefined,
|
||||
timeWindowMinutes: query.timeWindow
|
||||
? parseInt(query.timeWindow)
|
||||
: 60,
|
||||
limit: query.limit ? parseInt(query.limit) : 50,
|
||||
offset: query.offset ? parseInt(query.offset) : 0,
|
||||
});
|
||||
return reply.send(result);
|
||||
});
|
||||
const query = request.query as Record<string, string | number>;
|
||||
const result = await correlationService.getCorrelatedAlerts({
|
||||
userId,
|
||||
source: (query.source as any) || undefined,
|
||||
category: (query.category as any) || undefined,
|
||||
severity: (query.severity as any) || undefined,
|
||||
timeWindowMinutes:
|
||||
typeof query.timeWindow === "number" ? query.timeWindow : 60,
|
||||
limit: typeof query.limit === "number" ? query.limit : 50,
|
||||
offset: typeof query.offset === "number" ? query.offset : 0,
|
||||
});
|
||||
return reply.send(result);
|
||||
}
|
||||
);
|
||||
|
||||
fastify.post(
|
||||
"/ingest/darkwatch",
|
||||
|
||||
@@ -169,4 +169,9 @@ export async function reportRoutes(fastify: FastifyInstance) {
|
||||
const createdIds = await reportService.scheduleAnnualReports();
|
||||
return reply.code(200).send({ scheduled: createdIds.length, reportIds: createdIds });
|
||||
});
|
||||
|
||||
fastify.post('/schedule/weekly-digest', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const createdIds = await reportService.scheduleWeeklyDigest();
|
||||
return reply.code(200).send({ scheduled: createdIds.length, reportIds: createdIds });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import Fastify from "fastify";
|
||||
import cors from "@fastify/cors";
|
||||
import helmet from "@fastify/helmet";
|
||||
import sensible from "@fastify/sensible";
|
||||
import rawBody from "fastify-raw-body";
|
||||
import { extractOrGenerateRequestId } from "@shieldai/types";
|
||||
import { authMiddleware } from "./middleware/auth.middleware";
|
||||
import { errorHandlingMiddleware } from "./middleware/error-handling.middleware";
|
||||
@@ -16,8 +17,13 @@ import { extensionRoutes } from "./routes/extension.routes";
|
||||
import { waitlistRoutes } from "./routes/waitlist.routes";
|
||||
import { blogRoutes } from "./routes/blog.routes";
|
||||
import { blogAdminRoutes } from "./routes/blog-admin.routes";
|
||||
import { routes } from "./routes";
|
||||
import { captureSentryError } from "@shieldai/monitoring";
|
||||
import { getCorsOrigins } from "./config/api.config";
|
||||
import fastifySwagger from "@fastify/swagger";
|
||||
import fastifySwaggerUi from "@fastify/swagger-ui";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
const app = Fastify({
|
||||
logger: {
|
||||
@@ -30,6 +36,7 @@ async function bootstrap() {
|
||||
await app.register(cors, { origin: corsOrigins });
|
||||
await app.register(helmet);
|
||||
await app.register(sensible);
|
||||
await app.register(rawBody, { runFirst: true });
|
||||
|
||||
// Register auth middleware to populate request.user
|
||||
await app.register(authMiddleware);
|
||||
@@ -52,16 +59,54 @@ async function bootstrap() {
|
||||
request.headers["x-request-id"] = requestId;
|
||||
});
|
||||
|
||||
await app.register(darkwatchRoutes);
|
||||
await app.register(voiceprintRoutes);
|
||||
await app.register(correlationRoutes);
|
||||
await app.register(extensionRoutes, { prefix: '/extension' });
|
||||
await app.register(waitlistRoutes);
|
||||
await app.register(blogRoutes, { prefix: '/blog' });
|
||||
await app.register(blogAdminRoutes);
|
||||
await app.register(routes);
|
||||
|
||||
app.get("/health", async () => ({ status: "ok", timestamp: new Date().toISOString() }));
|
||||
|
||||
// Swagger/OpenAPI documentation
|
||||
const openapiSpec = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, "openapi", "spec.json"), "utf-8"),
|
||||
) as Record<string, unknown>;
|
||||
|
||||
const swaggerDefinition: Record<string, unknown> = {
|
||||
openapi: "3.0.3",
|
||||
info: {
|
||||
title: "ShieldAI API",
|
||||
description:
|
||||
"ShieldAI API documentation — reverse-engineer endpoints and run contract tests",
|
||||
version: "1.0.0",
|
||||
},
|
||||
servers: openapiSpec.servers,
|
||||
paths: openapiSpec.paths,
|
||||
components: openapiSpec.components,
|
||||
security: openapiSpec.security,
|
||||
tags: openapiSpec.tags,
|
||||
};
|
||||
|
||||
await app.register(fastifySwagger, {
|
||||
openapi: swaggerDefinition,
|
||||
});
|
||||
|
||||
await app.register(fastifySwaggerUi, {
|
||||
routePrefix: "/docs",
|
||||
uiConfig: {
|
||||
docExpansion: "list",
|
||||
},
|
||||
staticCSP: true,
|
||||
theme: {
|
||||
js: [
|
||||
{
|
||||
filename: "custom.js",
|
||||
content: `
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelector('link[rel="icon"]')?.remove();
|
||||
});
|
||||
`,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await app.listen({ port: parseInt(process.env.PORT || "3000", 10), host: "0.0.0.0" });
|
||||
app.log.info(`Server listening on port ${process.env.PORT || 3000}`);
|
||||
|
||||
Reference in New Issue
Block a user