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: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { sessionsRouter } from "./routes/me/sessions";
import { notificationsRouter } from "./routes/notifications";
import { socialRouter } from "./routes/social";
import { adminAuditRouter } from "./routes/admin/audit";
import { adminAuditExportRouter } from "./routes/admin/audit/export";
import { adminMarketsRouter } from "./routes/admin/markets";
import { adminSchemaVersionsRouter } from "./routes/admin/schema-versions";
import { errorHandler } from "./middleware/errorHandler";
Expand Down Expand Up @@ -124,6 +125,7 @@ export function createApp(): express.Express {
app.use("/api/me/devices", devicesRouter);
app.use("/api/me/sessions", sessionsRouter);
app.use("/api/admin/audit", adminAuditRouter);
app.use("/api/admin/audit", adminAuditExportRouter);
app.use("/api/admin/users", adminUsersRouter);
app.use("/api/admin/feature-flags", adminFeatureFlagsRouter);
app.use('/feature-flags', featureFlagsRouter);
Expand Down
79 changes: 2 additions & 77 deletions src/routes/admin/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,83 +46,8 @@ export function createAdminAuditRouter(opts: AdminAuditRouterOptions = {}): Rout

router.use(requireAdmin);

// Mount search handler to clear unused variable lint error
router.get("/search", searchAuditLogsHandler);

router.get("/", async (req, res, next) => {
try {
const parseResult = auditQuerySchema.safeParse(req.query);
if (!parseResult.success) {
throw RouteErrorFactory.validation(
parseResult.error.issues[0]?.message ?? "invalid query parameters",
);
}

const filters = parseResult.data;
const page = await getAuditLogs(filters);

res.json({
data: page.data,
nextCursor: page.nextCursor,
});
} catch (e) {
next(e);
}
});

return router;
}

export const adminAuditRouter = createAdminAuditRouter();import { Router } from "express";
import { rateLimit } from "express-rate-limit";
import { z } from "zod";
import { requireAdmin } from "../../middleware/requireAdmin";
import { getAuditLogs } from "../../repositories/auditLogRepo";
import { RouteErrorFactory } from "../../errors";
import { searchAuditLogsHandler } from "./audit/search";

export interface AdminAuditRouterOptions {
rateLimitPerMinute?: number;
}

const auditQuerySchema = z.object({
action: z.string().optional(),
actor: z.string().optional(),
startDate: z.string()
.datetime({ message: "startDate must be a valid ISO 8601 datetime string" })
.transform((val) => new Date(val))
.optional(),
endDate: z.string()
.datetime({ message: "endDate must be a valid ISO 8601 datetime string" })
.transform((val) => new Date(val))
.optional(),
cursor: z.string().optional(),
limit: z.string()
.regex(/^\d+$/, { message: "limit must be a positive integer" })
.transform((val) => parseInt(val, 10))
.optional(),
});

export function createAdminAuditRouter(opts: AdminAuditRouterOptions = {}): Router {
const router = Router();
const limit = opts.rateLimitPerMinute ?? 60;

router.use(
rateLimit({
windowMs: 60_000,
limit,
keyGenerator: (req) =>
(req.headers.authorization as string | undefined) ?? req.ip ?? "unknown",
standardHeaders: "draft-6",
legacyHeaders: false,
message: { error: { code: "rate_limit_exceeded" } },
}),
);

router.use(requireAdmin);

// Mount search handler to clear unused variable lint error
router.get("/search", searchAuditLogsHandler);
// Mount search handler (POST — parses req.body)
router.post("/search", searchAuditLogsHandler);

router.get("/", async (req, res, next) => {
try {
Expand Down