|
29 | 29 | const response = await fetch("/proxy/obp/v6.0.0/banks"); |
30 | 30 | const result = await response.json(); |
31 | 31 | if (result.banks) { |
32 | | - banks = result.banks.map((b: any) => b.id).sort(); |
| 32 | + banks = result.banks.map((b: any) => b.bank_id).sort(); |
33 | 33 | } |
34 | 34 | } catch (err) { |
35 | 35 | console.error("Error fetching banks:", err); |
|
46 | 46 | let providers = $state<string[]>([]); |
47 | 47 | let selectedProvider = $state(""); |
48 | 48 | let searchQuery = $state(""); |
| 49 | + let lastSearchedQuery = $state(""); |
49 | 50 | let searchResults = $state<any[]>([]); |
50 | 51 | let searchError = $state<string | null>(null); |
51 | 52 | let isSearching = $state(false); |
52 | 53 | let searchType = $state<"email" | "userid" | "username" | "">(""); |
| 54 | + let lastSearchCall = $state<{ proxyUrl: string; obpPath: string; status?: number; responseBody?: string } | null>(null); |
53 | 55 |
|
54 | 56 | // Fetch providers on mount |
55 | 57 | $effect(() => { |
56 | 58 | async function fetchProviders() { |
57 | 59 | try { |
58 | | - const response = await fetch("/backend/users/providers"); |
| 60 | + const response = await fetch("/proxy/obp/v6.0.0/providers"); |
59 | 61 | const result = await response.json(); |
60 | 62 | if (result.providers) { |
61 | 63 | providers = result.providers; |
62 | | - if (providers.length > 0) { |
63 | | - selectedProvider = providers[0]; |
64 | | - } |
65 | 64 | } |
66 | 65 | } catch (err) { |
67 | 66 | console.error("Error fetching providers:", err); |
|
91 | 90 | searchResults = []; |
92 | 91 | searchError = null; |
93 | 92 | searchType = ""; |
| 93 | + lastSearchedQuery = ""; |
94 | 94 | return; |
95 | 95 | } |
96 | 96 |
|
97 | 97 | const type = detectSearchType(searchQuery.trim()); |
98 | 98 | searchType = type; |
99 | 99 | isSearching = true; |
100 | 100 | searchError = null; |
| 101 | + lastSearchedQuery = searchQuery.trim(); |
| 102 | +
|
| 103 | + const params = new URLSearchParams(); |
| 104 | + if (type === "email") { |
| 105 | + params.set("email", searchQuery); |
| 106 | + } else if (type === "userid") { |
| 107 | + params.set("user_id", searchQuery); |
| 108 | + } else { |
| 109 | + params.set("username", searchQuery); |
| 110 | + if (selectedProvider) { |
| 111 | + params.set("provider", selectedProvider); |
| 112 | + } |
| 113 | + } |
| 114 | + const proxyUrl = `/proxy/obp/v6.0.0/users?${params.toString()}`; |
| 115 | +
|
| 116 | + const obpPath = proxyUrl.replace(/^\/proxy/, ""); |
| 117 | + lastSearchCall = { proxyUrl, obpPath }; |
101 | 118 |
|
102 | 119 | try { |
103 | | - let response; |
104 | | -
|
105 | | - if (type === "email") { |
106 | | - // Search by email (OBPv4.0.0) |
107 | | - response = await fetch( |
108 | | - `/proxy/obp/v4.0.0/users/email/${encodeURIComponent(searchQuery)}/terminator`, |
109 | | - ); |
110 | | - } else if (type === "userid") { |
111 | | - // Search by user ID (OBPv6.0.0) |
112 | | - response = await fetch( |
113 | | - `/proxy/obp/v6.0.0/users/user-id/${encodeURIComponent(searchQuery)}`, |
114 | | - ); |
115 | | - } else { |
116 | | - // Search by provider and username (OBPv6.0.0) |
117 | | - if (!selectedProvider) { |
118 | | - console.error("Provider is required for username search"); |
119 | | - searchResults = []; |
120 | | - isSearching = false; |
121 | | - return; |
122 | | - } |
123 | | - response = await fetch( |
124 | | - `/proxy/obp/v6.0.0/users/provider/${encodeURIComponent(selectedProvider)}/username/${encodeURIComponent(searchQuery)}`, |
125 | | - ); |
126 | | - } |
| 120 | + const response = await fetch(proxyUrl); |
| 121 | + const responseText = await response.text(); |
| 122 | + lastSearchCall = { proxyUrl, obpPath, status: response.status, responseBody: responseText }; |
127 | 123 |
|
128 | | - const result = await response.json(); |
| 124 | + const result = responseText ? JSON.parse(responseText) : {}; |
129 | 125 |
|
130 | 126 | if (!response.ok) { |
131 | | - searchError = result.error || `Search failed (HTTP ${response.status})`; |
| 127 | + if (typeof result.message !== "string") { |
| 128 | + throw new Error( |
| 129 | + `OBP error response missing 'message' field (HTTP ${response.status})`, |
| 130 | + ); |
| 131 | + } |
| 132 | + searchError = result.message; |
132 | 133 | searchResults = []; |
133 | 134 | return; |
134 | 135 | } |
135 | 136 |
|
136 | | - if (result.users) { |
137 | | - // Multiple results (email search, username search) |
138 | | - searchResults = result.users; |
139 | | - } else if (result.user_id) { |
140 | | - // Single user object returned directly by OBP (user ID or provider/username search) |
141 | | - searchResults = [result]; |
142 | | - } else { |
143 | | - searchResults = []; |
144 | | - } |
| 137 | + searchResults = result.users; |
145 | 138 | } catch (err) { |
146 | 139 | console.error("Search error:", err); |
147 | 140 | searchError = err instanceof Error ? err.message : "Search failed — the API may be unavailable"; |
|
209 | 202 | bind:value={selectedProvider} |
210 | 203 | class="form-input w-full" |
211 | 204 | > |
212 | | - {#if providers.length === 0} |
213 | | - <option value="">Loading providers...</option> |
214 | | - {:else} |
215 | | - {#each providers as provider} |
216 | | - <option value={provider}>{provider}</option> |
217 | | - {/each} |
218 | | - {/if} |
| 205 | + <option value="">Any</option> |
| 206 | + {#each providers as provider} |
| 207 | + <option value={provider}>{provider}</option> |
| 208 | + {/each} |
219 | 209 | </select> |
220 | 210 | </div> |
221 | 211 | <div class="flex-1"> |
|
239 | 229 | </button> |
240 | 230 | </form> |
241 | 231 |
|
| 232 | + {#snippet technicalDetails()} |
| 233 | + {#if lastSearchCall} |
| 234 | + <details class="search-call-details" data-testid="last-search-call"> |
| 235 | + <summary class="search-call-summary" data-testid="last-search-call-toggle">Query</summary> |
| 236 | + <div class="search-call-info"> |
| 237 | + <div class="search-call-field"> |
| 238 | + <label for="last-search-proxy-url" class="search-call-label">Proxy URL</label> |
| 239 | + <input |
| 240 | + id="last-search-proxy-url" |
| 241 | + type="text" |
| 242 | + readonly |
| 243 | + value={lastSearchCall.proxyUrl} |
| 244 | + data-testid="last-search-proxy-url" |
| 245 | + class="search-call-input" |
| 246 | + onclick={(e) => (e.currentTarget as HTMLInputElement).select()} |
| 247 | + /> |
| 248 | + </div> |
| 249 | + <div class="search-call-field"> |
| 250 | + <label for="last-search-obp-path" class="search-call-label">OBP path</label> |
| 251 | + <input |
| 252 | + id="last-search-obp-path" |
| 253 | + type="text" |
| 254 | + readonly |
| 255 | + value={lastSearchCall.obpPath} |
| 256 | + data-testid="last-search-obp-path" |
| 257 | + class="search-call-input" |
| 258 | + onclick={(e) => (e.currentTarget as HTMLInputElement).select()} |
| 259 | + /> |
| 260 | + </div> |
| 261 | + {#if lastSearchCall.status !== undefined} |
| 262 | + <div class="search-call-field"> |
| 263 | + <span class="search-call-label">Status</span> |
| 264 | + <span class="search-call-status" data-testid="last-search-status">{lastSearchCall.status}</span> |
| 265 | + </div> |
| 266 | + {/if} |
| 267 | + {#if lastSearchCall.responseBody} |
| 268 | + <div class="search-call-field"> |
| 269 | + <label for="last-search-response-body" class="search-call-label">Response</label> |
| 270 | + <textarea |
| 271 | + id="last-search-response-body" |
| 272 | + readonly |
| 273 | + data-testid="last-search-response-body" |
| 274 | + class="search-call-textarea" |
| 275 | + onclick={(e) => (e.currentTarget as HTMLTextAreaElement).select()} |
| 276 | + rows="6" |
| 277 | + >{lastSearchCall.responseBody}</textarea> |
| 278 | + </div> |
| 279 | + {/if} |
| 280 | + </div> |
| 281 | + </details> |
| 282 | + {/if} |
| 283 | + {/snippet} |
| 284 | + |
242 | 285 | <form |
243 | 286 | onsubmit={(e) => { |
244 | 287 | e.preventDefault(); |
|
297 | 340 |
|
298 | 341 | {#if searchResults.length > 0} |
299 | 342 | <div class="mt-6"> |
300 | | - <h3 class="text-lg font-semibold mb-4"> |
301 | | - {searchResults.length === 1 |
302 | | - ? "Result" |
303 | | - : `Results (${searchResults.length})`} |
304 | | - </h3> |
| 343 | + <div class="search-results-header mb-4"> |
| 344 | + <h3 class="text-lg font-semibold"> |
| 345 | + {searchResults.length === 1 |
| 346 | + ? "Result" |
| 347 | + : `Results (${searchResults.length})`} |
| 348 | + </h3> |
| 349 | + {@render technicalDetails()} |
| 350 | + </div> |
305 | 351 | <div class="table-wrapper"> |
306 | 352 | <table class="users-table"> |
307 | 353 | <thead> |
|
339 | 385 | </div> |
340 | 386 | </div> |
341 | 387 | {:else if searchError} |
342 | | - <div class="mt-6 alert alert-error"> |
343 | | - <strong>Search error:</strong> {searchError} |
| 388 | + <div class="mt-6"> |
| 389 | + <div class="search-results-header mb-3"> |
| 390 | + <strong class="text-error">Search error</strong> |
| 391 | + {@render technicalDetails()} |
| 392 | + </div> |
| 393 | + <div class="alert alert-error">{searchError}</div> |
344 | 394 | </div> |
345 | | - {:else if searchQuery.trim() && !isSearching} |
346 | | - <div class="mt-6 text-center text-gray-500"> |
347 | | - No users found matching "{searchQuery}" |
| 395 | + {:else if lastSearchedQuery && !isSearching} |
| 396 | + <div class="mt-6"> |
| 397 | + <div class="search-results-header mb-3"> |
| 398 | + <span class="text-gray-500">No users found matching "{lastSearchedQuery}"</span> |
| 399 | + {@render technicalDetails()} |
| 400 | + </div> |
348 | 401 | </div> |
349 | 402 | {/if} |
350 | 403 | </div> |
|
609 | 662 | color: rgb(var(--color-error-200)); |
610 | 663 | border-color: rgb(var(--color-error-800)); |
611 | 664 | } |
| 665 | +
|
| 666 | + .search-results-header { |
| 667 | + display: flex; |
| 668 | + align-items: flex-start; |
| 669 | + justify-content: space-between; |
| 670 | + gap: 1rem; |
| 671 | + flex-wrap: wrap; |
| 672 | + } |
| 673 | +
|
| 674 | + .search-call-details { |
| 675 | + font-size: 0.8125rem; |
| 676 | + } |
| 677 | +
|
| 678 | + .search-call-details[open] { |
| 679 | + flex-basis: 100%; |
| 680 | + } |
| 681 | +
|
| 682 | + .search-call-summary { |
| 683 | + padding: 0.25rem 0; |
| 684 | + cursor: pointer; |
| 685 | + font-weight: 600; |
| 686 | + color: #6b7280; |
| 687 | + user-select: none; |
| 688 | + } |
| 689 | +
|
| 690 | + :global([data-mode="dark"]) .search-call-summary { |
| 691 | + color: var(--color-surface-400); |
| 692 | + } |
| 693 | +
|
| 694 | + .search-call-info { |
| 695 | + display: flex; |
| 696 | + flex-direction: column; |
| 697 | + gap: 0.5rem; |
| 698 | + padding: 0.5rem 0; |
| 699 | + } |
| 700 | +
|
| 701 | + .search-call-field { |
| 702 | + display: flex; |
| 703 | + flex-direction: column; |
| 704 | + gap: 0.25rem; |
| 705 | + } |
| 706 | +
|
| 707 | + .search-call-label { |
| 708 | + color: #6b7280; |
| 709 | + font-weight: 600; |
| 710 | + font-size: 0.75rem; |
| 711 | + text-transform: uppercase; |
| 712 | + letter-spacing: 0.03em; |
| 713 | + } |
| 714 | +
|
| 715 | + :global([data-mode="dark"]) .search-call-label { |
| 716 | + color: var(--color-surface-400); |
| 717 | + } |
| 718 | +
|
| 719 | + .search-call-input, |
| 720 | + .search-call-textarea { |
| 721 | + width: 100%; |
| 722 | + padding: 0.375rem 0.5rem; |
| 723 | + background: white; |
| 724 | + border: 1px solid #d1d5db; |
| 725 | + border-radius: 0.25rem; |
| 726 | + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; |
| 727 | + font-size: 0.8125rem; |
| 728 | + color: #111827; |
| 729 | + } |
| 730 | +
|
| 731 | + :global([data-mode="dark"]) .search-call-input, |
| 732 | + :global([data-mode="dark"]) .search-call-textarea { |
| 733 | + background: rgb(var(--color-surface-800)); |
| 734 | + border-color: rgb(var(--color-surface-600)); |
| 735 | + color: var(--color-surface-100); |
| 736 | + } |
| 737 | +
|
| 738 | + .search-call-textarea { |
| 739 | + resize: vertical; |
| 740 | + min-height: 4rem; |
| 741 | + white-space: pre-wrap; |
| 742 | + word-break: break-all; |
| 743 | + } |
| 744 | +
|
| 745 | + .search-call-status { |
| 746 | + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; |
| 747 | + font-size: 0.8125rem; |
| 748 | + } |
612 | 749 | </style> |
0 commit comments