Spaces:
Paused
Paused
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
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 91 |
app.get("/auth/accounts/export", (c) => {
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|