icebear0828 commited on
Commit
a8a2d95
Β·
1 Parent(s): 268c5a4

fix: server-side export filtering and JSON parse safety

Browse files

- Export endpoint supports ?ids= query param for selective export,
preventing full token payload over the network
- Import hook wraps JSON.parse in try-catch for invalid files
- Add test for selective export filtering

shared/hooks/use-accounts.ts CHANGED
@@ -156,13 +156,11 @@ export function useAccounts() {
156
  }, []);
157
 
158
  const exportAccounts = useCallback(async (selectedIds?: string[]) => {
159
- const resp = await fetch("/auth/accounts/export");
 
 
 
160
  const data = await resp.json() as { accounts: Array<{ id: string }> };
161
- // Filter to selected accounts if specified
162
- if (selectedIds && selectedIds.length > 0) {
163
- const idSet = new Set(selectedIds);
164
- data.accounts = data.accounts.filter((a) => idSet.has(a.id));
165
- }
166
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
167
  const url = URL.createObjectURL(blob);
168
  const a = document.createElement("a");
@@ -184,7 +182,12 @@ export function useAccounts() {
184
  errors: string[];
185
  }> => {
186
  const text = await file.text();
187
- const parsed = JSON.parse(text) as Record<string, unknown>;
 
 
 
 
 
188
  // Support both { accounts: [...] } (export format) and raw array
189
  const accounts = Array.isArray(parsed)
190
  ? parsed
 
156
  }, []);
157
 
158
  const exportAccounts = useCallback(async (selectedIds?: string[]) => {
159
+ const params = selectedIds && selectedIds.length > 0
160
+ ? `?ids=${selectedIds.join(",")}`
161
+ : "";
162
+ const resp = await fetch(`/auth/accounts/export${params}`);
163
  const data = await resp.json() as { accounts: Array<{ id: string }> };
 
 
 
 
 
164
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
165
  const url = URL.createObjectURL(blob);
166
  const a = document.createElement("a");
 
182
  errors: string[];
183
  }> => {
184
  const text = await file.text();
185
+ let parsed: unknown;
186
+ try {
187
+ parsed = JSON.parse(text);
188
+ } catch {
189
+ return { success: false, added: 0, updated: 0, failed: 0, errors: ["Invalid JSON file"] };
190
+ }
191
  // Support both { accounts: [...] } (export format) and raw array
192
  const accounts = Array.isArray(parsed)
193
  ? parsed
src/routes/__tests__/accounts-import-export.test.ts CHANGED
@@ -108,6 +108,17 @@ describe("account import/export", () => {
108
  }
109
  });
110
 
 
 
 
 
 
 
 
 
 
 
 
111
  // ── Import ─────────────────────────────────────────────
112
 
113
  it("POST /auth/accounts/import adds new accounts", async () => {
 
108
  }
109
  });
110
 
111
+ it("GET /auth/accounts/export?ids= filters server-side", async () => {
112
+ const id1 = pool.addAccount("tokenAAAA1234567890");
113
+ pool.addAccount("tokenBBBB1234567890");
114
+
115
+ const res = await app.request(`/auth/accounts/export?ids=${id1}`);
116
+ expect(res.status).toBe(200);
117
+ const data = await res.json() as { accounts: Array<{ id: string }> };
118
+ expect(data.accounts).toHaveLength(1);
119
+ expect(data.accounts[0].id).toBe(id1);
120
+ });
121
+
122
  // ── Import ─────────────────────────────────────────────
123
 
124
  it("POST /auth/accounts/import adds new accounts", async () => {
src/routes/accounts.ts CHANGED
@@ -87,9 +87,15 @@ export function createAccountRoutes(
87
  return c.redirect(authUrl);
88
  });
89
 
90
- // Export all accounts (with tokens) for backup/migration
 
91
  app.get("/auth/accounts/export", (c) => {
92
- const entries = pool.getAllEntries();
 
 
 
 
 
93
  return c.json({ accounts: entries });
94
  });
95
 
 
87
  return c.redirect(authUrl);
88
  });
89
 
90
+ // Export accounts (with tokens) for backup/migration
91
+ // ?ids=id1,id2 for selective export; omit for all
92
  app.get("/auth/accounts/export", (c) => {
93
+ let entries = pool.getAllEntries();
94
+ const idsParam = c.req.query("ids");
95
+ if (idsParam) {
96
+ const idSet = new Set(idsParam.split(",").filter(Boolean));
97
+ entries = entries.filter((e) => idSet.has(e.id));
98
+ }
99
  return c.json({ accounts: entries });
100
  });
101