feat: add CSV/JSON export, advanced bulk filters & selection to Catalog Manager
- Admin Audit Log: CSV/JSON export via ?format=query param - Catalog Manager: club_only / have_offer filters (multi-filter support) - Offer discovery: refactored discoverOffers() to accept filters object - Translations added for new filter labels - Test updates for all modified paths
This commit is contained in:
1 parent
eeca532057
commit
388d8992f8
8 files changed
+279
-62
No files matched your search
@@ -31,7 +31,7 @@ vi.mock("@/lib/services/audit", () => ({
|
||||
import { getAuditLogs } from "@/lib/services/audit";
|
||||
import { GET } from "./route";
|
||||
|
||||
describe("audit CSV", () => {
|
||||
describe("audit export", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(getAuditLogs).mockClear();
|
||||
});
|
||||
@@ -54,6 +54,19 @@ describe("audit CSV", () => {
|
||||
});
|
||||
expect(response.headers.get("Cache-Control")).toBe("no-store");
|
||||
});
|
||||
it("supports JSON export format", async () => {
|
||||
const response = await GET(
|
||||
new Request(
|
||||
"https://example.test/api/admin/logs/audit/export?format=json",
|
||||
) as never,
|
||||
{} as never,
|
||||
);
|
||||
expect(response.headers.get("Content-Type")).toContain("application/json");
|
||||
const json = await response.json();
|
||||
expect(Array.isArray(json)).toBe(true);
|
||||
expect(json[0].id).toBe(1);
|
||||
expect(json[0].username).toBe('=HYPERLINK("bad")');
|
||||
});
|
||||
it("escapes CSV formula cells and redacts historical secrets", async () => {
|
||||
const response = await GET(
|
||||
new Request("https://example.test/api/admin/logs/audit/export") as never,
|
||||
|
||||
@@ -23,6 +23,7 @@ export const GET = withAdmin(
|
||||
from: params.get("from") ?? undefined,
|
||||
to: params.get("to") ?? undefined,
|
||||
});
|
||||
const format = params.get("format") || "csv";
|
||||
const rows: unknown[][] = [
|
||||
[
|
||||
"id",
|
||||
@@ -35,8 +36,17 @@ export const GET = withAdmin(
|
||||
"changes",
|
||||
],
|
||||
];
|
||||
const jsonItems = [];
|
||||
for (const row of result.rows) {
|
||||
const details = readAuditChanges(row.diff, row.before, row.after);
|
||||
const changesFormatted = details.invalid
|
||||
? "[Unavailable legacy details]"
|
||||
: details.changes
|
||||
.map(
|
||||
(change) =>
|
||||
`${change.key}: ${formatAuditValue(change.from)} → ${formatAuditValue(change.to)}`,
|
||||
)
|
||||
.join("\n");
|
||||
rows.push([
|
||||
row.id,
|
||||
row.userId,
|
||||
@@ -45,15 +55,27 @@ export const GET = withAdmin(
|
||||
row.target,
|
||||
row.targetId,
|
||||
row.createdAt,
|
||||
details.invalid
|
||||
? "[Unavailable legacy details]"
|
||||
: details.changes
|
||||
.map(
|
||||
(change) =>
|
||||
`${change.key}: ${formatAuditValue(change.from)} → ${formatAuditValue(change.to)}`,
|
||||
)
|
||||
.join("\n"),
|
||||
changesFormatted,
|
||||
]);
|
||||
jsonItems.push({
|
||||
id: row.id,
|
||||
userId: row.userId,
|
||||
username: row.username,
|
||||
action: row.action,
|
||||
target: row.target,
|
||||
targetId: row.targetId,
|
||||
createdAt: row.createdAt,
|
||||
changes: details.changes,
|
||||
});
|
||||
}
|
||||
if (format === "json") {
|
||||
return new Response(JSON.stringify(jsonItems, null, 2), {
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Content-Disposition": 'attachment; filename="audit-page.json"',
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
});
|
||||
}
|
||||
return new Response(
|
||||
`\uFEFF${rows.map((row) => row.map(cell).join(",")).join("\r\n")}`,
|
||||
|
||||
Reference in new issue
Block a user