mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 12:52:11 +03:00
Mirrors the 9router UX (one textarea, name|apiKey per line) but goes
further: dedicated server-side endpoint with Zod validation, partial-
failure semantics, audit log, and provider whitelist.
UI (src/app/(dashboard)/dashboard/providers/[id]/page.tsx):
- AddApiKeyModal now switches between Single (existing behaviour) and
Bulk Add via tab strip. Tabs hide for providers that don't support
bulk (Vertex, web-session, OAuth, multi-field).
- Bulk pane: textarea, shared Priority + "validate each key" checkbox,
result panel with per-line errors (truncated at 10).
Backend:
- POST /api/providers/bulk: iterates entries through createProviderConnection
with the same provider-specific normalization as the single endpoint.
Returns {success, failed, total, created, errors[]}. Optional pre-save
validation via /api/providers/validate when validateKeys=true. Each
entry succeeds/fails independently — no transaction rollback.
- Bulk audit event logged once per request plus per-entry success events.
Schemas:
- bulkCreateProviderSchema (src/shared/validation/schemas.ts): max 200
entries, mandatory name+apiKey per entry, google-pse-search cx guard.
- supportsBulkApiKey() helper (src/shared/constants/providers.ts) with
explicit deny-list for OAuth/web-session/multi-field providers.
Parser:
- parseBulkApiKeys() (src/shared/utils/bulkApiKeyParser.ts) handles
CRLF, # comments, blank lines, pipe inside apiKey, empty-name fallback,
and caps input at BULK_API_KEY_MAX_LINES (200) with a warning.
Tests:
- tests/unit/bulkApiKeyParser.test.ts: 12 cases (format, edge cases, cap)
- tests/unit/providers-bulk-route.test.ts: 12 cases (schema, whitelist,
response shape, apiKey leak guard)
i18n:
- en.json: bulkTabSingle, bulkTabBulkAdd, bulkAddFormatHint,
bulkValidateKeys, bulkAddAllKeys, bulkAddedCount, bulkFailedCount,
adding
68 lines
1.6 KiB
TypeScript
68 lines
1.6 KiB
TypeScript
/**
|
|
* Parses textarea input for bulk API key creation.
|
|
*
|
|
* Supported line formats (one per line):
|
|
* - `name|apiKey`
|
|
* - `apiKey` (auto-named as `Key N`)
|
|
* - `# comment` (skipped)
|
|
* - blank lines (skipped)
|
|
*
|
|
* `apiKey` may contain `|` — only the first `|` is treated as the separator.
|
|
*/
|
|
|
|
export interface BulkApiKeyEntry {
|
|
name: string;
|
|
apiKey: string;
|
|
lineNumber: number;
|
|
}
|
|
|
|
export interface BulkApiKeyParseResult {
|
|
entries: BulkApiKeyEntry[];
|
|
warnings: string[];
|
|
}
|
|
|
|
const MAX_BULK_LINES = 200;
|
|
|
|
export function parseBulkApiKeys(text: string): BulkApiKeyParseResult {
|
|
const lines = text.split(/\r?\n/);
|
|
const entries: BulkApiKeyEntry[] = [];
|
|
const warnings: string[] = [];
|
|
let autoIdx = 1;
|
|
|
|
if (lines.length > MAX_BULK_LINES) {
|
|
warnings.push(
|
|
`Input has ${lines.length} lines; only the first ${MAX_BULK_LINES} will be processed.`
|
|
);
|
|
}
|
|
|
|
const bound = Math.min(lines.length, MAX_BULK_LINES);
|
|
for (let i = 0; i < bound; i++) {
|
|
const raw = lines[i].trim();
|
|
if (!raw) continue;
|
|
if (raw.startsWith("#")) continue;
|
|
|
|
const pipeIdx = raw.indexOf("|");
|
|
let name: string;
|
|
let apiKey: string;
|
|
if (pipeIdx === -1) {
|
|
name = `Key ${autoIdx++}`;
|
|
apiKey = raw;
|
|
} else {
|
|
const namePart = raw.slice(0, pipeIdx).trim();
|
|
apiKey = raw.slice(pipeIdx + 1).trim();
|
|
name = namePart || `Key ${autoIdx++}`;
|
|
}
|
|
|
|
if (!apiKey) {
|
|
warnings.push(`Line ${i + 1}: empty apiKey, skipped`);
|
|
continue;
|
|
}
|
|
|
|
entries.push({ name, apiKey, lineNumber: i + 1 });
|
|
}
|
|
|
|
return { entries, warnings };
|
|
}
|
|
|
|
export const BULK_API_KEY_MAX_LINES = MAX_BULK_LINES;
|