Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/src/api/middleware/costRateLimit.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ async function getDebtAdjustedBudget(
if (nextDebt < 1) {
await redis.del(debtKey);
} else {
await redis.set(debtKey, String(nextDebt), { PX: windowMs * 2 });
await redis.psetex(debtKey, windowMs * 2, String(nextDebt));
}
return Math.max(1, baseBudget - reduction);
} catch {
Expand Down
131 changes: 131 additions & 0 deletions backend/src/api/routes/adminImpersonation.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import type { FastifyInstance } from "fastify";
import {
adminImpersonationService,
type ImpersonationStatus,
} from "../../services/adminImpersonation.service.js";
import { sendApiError } from "../utils/response.js";
import { authMiddleware } from "../middleware/auth.js";

interface StartImpersonationBody {
adminId?: string;
impersonatedUserId: string;
reason: string;
approvalTicketId?: string;
durationMinutes?: number;
}

interface StopImpersonationBody {
sessionId: string;
adminId?: string;
}

export async function adminImpersonationRoutes(server: FastifyInstance) {
const requireAdmin = authMiddleware({ requiredScopes: ["admin:access"] });

// Start impersonation session
server.post<{ Body: StartImpersonationBody }>(
"/start",
{ preHandler: requireAdmin },
async (request, reply) => {
const { adminId, impersonatedUserId, reason, approvalTicketId, durationMinutes } =
request.body;

const resolvedAdminId = adminId ?? request.apiKeyAuth?.name ?? "admin";
const ipAddress = request.ip ?? "127.0.0.1";

if (!impersonatedUserId?.trim()) {
return sendApiError(reply, 400, "impersonatedUserId is required");
}
if (!reason?.trim()) {
return sendApiError(
reply,
400,
"Mandatory justification reason is required for admin impersonation"
);
}

try {
const result = await adminImpersonationService.startSession({
adminId: resolvedAdminId,
impersonatedUserId,
reason,
approvalTicketId,
durationMinutes,
ipAddress,
});

return reply.code(201).send(result);
} catch (error) {
const message =
error instanceof Error ? error.message : "Failed to start impersonation";
return sendApiError(reply, 400, message);
}
}
);

// Stop impersonation session
server.post<{ Body: StopImpersonationBody }>(
"/stop",
{ preHandler: requireAdmin },
async (request, reply) => {
const { sessionId, adminId } = request.body;
const resolvedAdminId = adminId ?? request.apiKeyAuth?.name ?? "admin";

if (!sessionId?.trim()) {
return sendApiError(reply, 400, "sessionId is required");
}

const session = await adminImpersonationService.endSession(
sessionId,
resolvedAdminId
);

if (!session) {
return sendApiError(reply, 404, "Active impersonation session not found");
}

return { session };
}
);

// List impersonation sessions
server.get<{
Querystring: {
adminId?: string;
impersonatedUserId?: string;
status?: ImpersonationStatus;
limit?: string;
offset?: string;
};
}>(
"/sessions",
{ preHandler: requireAdmin },
async (request) => {
const sessions = await adminImpersonationService.listSessions({
adminId: request.query.adminId,
impersonatedUserId: request.query.impersonatedUserId,
status: request.query.status,
limit: request.query.limit ? Number(request.query.limit) : undefined,
offset: request.query.offset ? Number(request.query.offset) : undefined,
});

return { sessions };
}
);

// Get audit logs for session
server.get<{ Querystring: { sessionId: string } }>(
"/audit-logs",
{ preHandler: requireAdmin },
async (request, reply) => {
const { sessionId } = request.query;

if (!sessionId?.trim()) {
return sendApiError(reply, 400, "sessionId is required");
}

const auditLogs = await adminImpersonationService.getAuditLogs(sessionId);
return { auditLogs };
}
);
}
22 changes: 9 additions & 13 deletions backend/src/api/routes/alertNoiseReduction.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { getPaginationParams, formatPaginatedResponse } from "../../utils/pagina
export async function alertNoiseReductionRoutes(server: FastifyInstance) {
server.addHook("preHandler", authMiddleware());

server.post<{ Body: { accountId: string; alertRuleId: string; windowStart: string; windowEnd: string } }>(
server.post<{ Body: { accountId: string; alertRuleId: string; windowStart: string; windowEnd: string; sampleSize?: number } }>(
"/analyses",
{
schema: {
Expand Down Expand Up @@ -36,7 +36,7 @@ export async function alertNoiseReductionRoutes(server: FastifyInstance) {
},
},
},
async (request: FastifyRequest, reply: FastifyReply) => {
async (request: FastifyRequest<{ Body: { accountId: string; alertRuleId: string; windowStart: string; windowEnd: string; sampleSize?: number } }>, reply) => {
const { accountId, alertRuleId, windowStart, windowEnd, sampleSize } = request.body;

const result = await alertNoiseReductionService.analyzeAlertNoise({
Expand Down Expand Up @@ -65,14 +65,14 @@ export async function alertNoiseReductionRoutes(server: FastifyInstance) {
},
},
},
async (request: FastifyRequest, reply: FastifyReply) => {
async (request: FastifyRequest<{ Params: { analysisId: string } }>, reply) => {
const { analysisId } = request.params;
const result = await alertNoiseReductionService.getAnalysis(analysisId);
return reply.send(result);
},
);

server.get<{ Querystring: { limit?: string; offset?: string } }>(
server.get<{ Params: { accountId: string }; Querystring: { limit?: string; offset?: string } }>(
"/accounts/:accountId/analyses",
{
schema: {
Expand All @@ -93,18 +93,14 @@ export async function alertNoiseReductionRoutes(server: FastifyInstance) {
},
},
},
async (request: FastifyRequest, reply: FastifyReply) => {
async (request: FastifyRequest<{ Params: { accountId: string }; Querystring: { limit?: string; offset?: string } }>, reply) => {
const { accountId } = request.params;
const { limit, offset } = getPaginationParams(request.query as Record<string, string>);
const { limit: limitNum, offset, page } = getPaginationParams(request.query as any);

const result = await alertNoiseReductionService.listAnalyses(accountId, limit, offset);
const result = await alertNoiseReductionService.listAnalyses(accountId, limitNum, offset);

return reply.send(
formatPaginatedResponse(result.analyses, {
total: result.pagination.total,
limit,
offset,
}),
formatPaginatedResponse(result.analyses, Number(result.pagination.total), page, limitNum),
);
},
);
Expand All @@ -123,7 +119,7 @@ export async function alertNoiseReductionRoutes(server: FastifyInstance) {
},
},
},
async (request: FastifyRequest, reply: FastifyReply) => {
async (request: FastifyRequest<{ Params: { recommendationId: string } }>, reply) => {
const { recommendationId } = request.params;

const result = await alertNoiseReductionService.applyRecommendation(recommendationId);
Expand Down
107 changes: 107 additions & 0 deletions backend/src/api/routes/assetLifecycleTimeline.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { FastifyInstance } from "fastify";
import {
assetLifecycleTimelineService,
type AssetState,
} from "../../services/assetLifecycleTimeline.service.js";
import { sendApiError } from "../utils/response.js";
import { authMiddleware } from "../middleware/auth.js";

interface RecordTransitionBody {
assetId: string;
assetSymbol: string;
state: AssetState;
previousState?: AssetState;
reason?: string;
triggeredBy?: string;
metadata?: Record<string, unknown>;
}

export async function assetLifecycleTimelineRoutes(server: FastifyInstance) {
const requireAuth = authMiddleware();

// Record a new asset state transition
server.post<{ Body: RecordTransitionBody }>(
"/",
{ preHandler: requireAuth },
async (request, reply) => {
const { assetId, assetSymbol, state, previousState, reason, triggeredBy, metadata } =
request.body;

if (!assetId?.trim() || !assetSymbol?.trim()) {
return sendApiError(reply, 400, "assetId and assetSymbol are required");
}
if (!state) {
return sendApiError(reply, 400, "state is required");
}

try {
const record = await assetLifecycleTimelineService.recordTransition({
assetId,
assetSymbol,
state,
previousState,
reason,
triggeredBy: triggeredBy ?? request.apiKeyAuth?.name ?? "admin",
metadata,
});
return reply.code(201).send({ record });
} catch (error) {
const message = error instanceof Error ? error.message : "State transition failed";
return sendApiError(reply, 400, message);
}
}
);

// Get timeline entries with optional filters
server.get<{
Querystring: {
assetId?: string;
state?: AssetState;
startDate?: string;
endDate?: string;
limit?: string;
offset?: string;
};
}>(
"/",
{ preHandler: requireAuth },
async (request) => {
const records = await assetLifecycleTimelineService.getTimeline(
request.query.assetId,
{
state: request.query.state,
startDate: request.query.startDate,
endDate: request.query.endDate,
limit: request.query.limit ? Number(request.query.limit) : undefined,
offset: request.query.offset ? Number(request.query.offset) : undefined,
}
);
return { records };
}
);

// Get timeline stats
server.get(
"/stats",
{ preHandler: requireAuth },
async () => {
const stats = await assetLifecycleTimelineService.getStats();
return { stats };
}
);

// Get latest state for a given asset
server.get<{ Params: { assetId: string } }>(
"/latest/:assetId",
{ preHandler: requireAuth },
async (request, reply) => {
const record = await assetLifecycleTimelineService.getLatestState(
request.params.assetId
);
if (!record) {
return sendApiError(reply, 404, "No lifecycle history found for asset");
}
return { record };
}
);
}
4 changes: 2 additions & 2 deletions backend/src/api/routes/bftOracle.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function bftOracleRoutes(server: FastifyInstance) {
server.post("/aggregate", async (request: FastifyRequest<{ Body: z.infer<typeof aggregateSchema> }>, reply: FastifyReply) => {
try {
const { assetCode, reports } = aggregateSchema.parse(request.body);
const result = await bftOracleAggregatorService.aggregateBftState(assetCode, reports);
const result = await bftOracleAggregatorService.aggregateBftState(assetCode, reports as any);
return reply.code(200).send(result);
} catch (error) {
logger.error(error, "Failed to run BFT state aggregation");
Expand All @@ -38,7 +38,7 @@ export async function bftOracleRoutes(server: FastifyInstance) {
server.post("/providers", async (request: FastifyRequest<{ Body: z.infer<typeof registerNodeSchema> }>, reply: FastifyReply) => {
try {
const body = registerNodeSchema.parse(request.body);
const provider = await bftOracleAggregatorService.registerProviderNode(body);
const provider = await bftOracleAggregatorService.registerProviderNode(body as any);
return reply.code(201).send(provider);
} catch (error) {
logger.error(error, "Failed to register BFT provider node");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export async function complianceRetentionExceptionRoutes(
...parsed.data,
startDate: parsed.data.startDate ? new Date(parsed.data.startDate) : undefined,
endDate: parsed.data.endDate ? new Date(parsed.data.endDate) : undefined,
});
} as any);
return reply.code(201).send({ exception: record });
} catch (error) {
logger.error({ error }, "Failed to create compliance retention exception");
Expand Down
4 changes: 2 additions & 2 deletions backend/src/api/routes/connectivity.routes.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { FastifyInstance } from "fastify";
import { ChainConnectivityService } from "../../services/chainConnectivity.service.js";
import { getDb } from "../../database/connection.js";
import { getDatabase } from "../../database/connection.js";

export async function connectivityRoutes(fastify: FastifyInstance) {
const db = getDb();
const db = getDatabase();
const service = new ChainConnectivityService(db);

fastify.get(
Expand Down
2 changes: 1 addition & 1 deletion backend/src/api/routes/contractStorageFootprint.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export async function contractStorageFootprintRoutes(server: FastifyInstance) {
) => {
try {
const body = snapshotBodySchema.parse(request.body);
const snapshot = await contractStorageFootprintService.recordSnapshot(body);
const snapshot = await contractStorageFootprintService.recordSnapshot(body as any);
reply.code(201);
return { success: true, snapshot };
} catch (error) {
Expand Down
Loading
Loading