Skip to content
This repository was archived by the owner on May 12, 2026. It is now read-only.

Commit fef7f91

Browse files
committed
Users search
1 parent bce3edd commit fef7f91

4 files changed

Lines changed: 206 additions & 137 deletions

File tree

.env.example

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ ORIGIN=http://localhost:3003
22

33
# OBP API Configuration
44
PUBLIC_OBP_BASE_URL="http://localhost:8080"
5-
OBP_API_HOST=localhost:8080
6-
OBP_API_URL=http://localhost:8080
7-
VITE_API_URL=http://localhost:8080
85

96
# OIDC Configuration
107
OBP_OAUTH_CLIENT_ID=2a47cc56-0db1-409d-8f0b-131e4f94a212

src/lib/components/UserSearchPickerWidget.svelte

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@
5252
async function fetchProviders() {
5353
isLoadingProviders = true;
5454
try {
55-
const response = await trackedFetch("/backend/users/providers");
55+
const response = await trackedFetch("/proxy/obp/v6.0.0/providers");
5656
if (response.ok) {
5757
const data = await response.json();
58-
providers = data.providers || [];
58+
providers = data.providers;
5959
}
6060
} catch (error) {
6161
console.error("Failed to fetch providers:", error);
@@ -105,17 +105,18 @@
105105
106106
try {
107107
let url: string;
108-
let usesProviderEndpoint = false;
109108
if (type === "email") {
110109
url = `/proxy/obp/v4.0.0/users/email/${encodeURIComponent(trimmed)}/terminator`;
111110
} else if (type === "userid") {
112111
url = `/proxy/obp/v6.0.0/users/user-id/${encodeURIComponent(trimmed)}`;
113-
} else if (selectedProvider) {
114-
// Use provider+username endpoint when a provider is selected
115-
url = `/proxy/obp/v6.0.0/users/provider/${encodeURIComponent(selectedProvider)}/username/${encodeURIComponent(trimmed)}`;
116-
usesProviderEndpoint = true;
117112
} else {
118-
url = `/proxy/obp/v6.0.0/users?username=${encodeURIComponent(trimmed)}`;
113+
// Username search via /obp/v6.0.0/users with query-param filters
114+
const params = new URLSearchParams();
115+
params.set("username", trimmed);
116+
if (selectedProvider) {
117+
params.set("provider", selectedProvider);
118+
}
119+
url = `/proxy/obp/v6.0.0/users?${params.toString()}`;
119120
}
120121
121122
const response = await trackedFetch(url);
@@ -133,12 +134,12 @@
133134
134135
const data = await response.json();
135136
136-
// Normalize response: provider and userid OBP endpoints return the user object directly, others return {users: [...]}
137+
// Normalize response: userid OBP endpoint returns the user object directly; others return {users: [...]}
137138
let users: UserResult[];
138-
if (type === "userid" || usesProviderEndpoint) {
139+
if (type === "userid") {
139140
users = data.user_id ? [data] : [];
140141
} else {
141-
users = data.users || [];
142+
users = data.users;
142143
}
143144
144145
// Filter out users without a username

src/routes/(protected)/users/+page.svelte

Lines changed: 194 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
const response = await fetch("/proxy/obp/v6.0.0/banks");
3030
const result = await response.json();
3131
if (result.banks) {
32-
banks = result.banks.map((b: any) => b.id).sort();
32+
banks = result.banks.map((b: any) => b.bank_id).sort();
3333
}
3434
} catch (err) {
3535
console.error("Error fetching banks:", err);
@@ -46,22 +46,21 @@
4646
let providers = $state<string[]>([]);
4747
let selectedProvider = $state("");
4848
let searchQuery = $state("");
49+
let lastSearchedQuery = $state("");
4950
let searchResults = $state<any[]>([]);
5051
let searchError = $state<string | null>(null);
5152
let isSearching = $state(false);
5253
let searchType = $state<"email" | "userid" | "username" | "">("");
54+
let lastSearchCall = $state<{ proxyUrl: string; obpPath: string; status?: number; responseBody?: string } | null>(null);
5355
5456
// Fetch providers on mount
5557
$effect(() => {
5658
async function fetchProviders() {
5759
try {
58-
const response = await fetch("/backend/users/providers");
60+
const response = await fetch("/proxy/obp/v6.0.0/providers");
5961
const result = await response.json();
6062
if (result.providers) {
6163
providers = result.providers;
62-
if (providers.length > 0) {
63-
selectedProvider = providers[0];
64-
}
6564
}
6665
} catch (err) {
6766
console.error("Error fetching providers:", err);
@@ -91,57 +90,51 @@
9190
searchResults = [];
9291
searchError = null;
9392
searchType = "";
93+
lastSearchedQuery = "";
9494
return;
9595
}
9696
9797
const type = detectSearchType(searchQuery.trim());
9898
searchType = type;
9999
isSearching = true;
100100
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 };
101118
102119
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 };
127123
128-
const result = await response.json();
124+
const result = responseText ? JSON.parse(responseText) : {};
129125
130126
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;
132133
searchResults = [];
133134
return;
134135
}
135136
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;
145138
} catch (err) {
146139
console.error("Search error:", err);
147140
searchError = err instanceof Error ? err.message : "Search failed — the API may be unavailable";
@@ -209,13 +202,10 @@
209202
bind:value={selectedProvider}
210203
class="form-input w-full"
211204
>
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}
219209
</select>
220210
</div>
221211
<div class="flex-1">
@@ -239,6 +229,59 @@
239229
</button>
240230
</form>
241231

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+
242285
<form
243286
onsubmit={(e) => {
244287
e.preventDefault();
@@ -297,11 +340,14 @@
297340

298341
{#if searchResults.length > 0}
299342
<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>
305351
<div class="table-wrapper">
306352
<table class="users-table">
307353
<thead>
@@ -339,12 +385,19 @@
339385
</div>
340386
</div>
341387
{: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>
344394
</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>
348401
</div>
349402
{/if}
350403
</div>
@@ -609,4 +662,88 @@
609662
color: rgb(var(--color-error-200));
610663
border-color: rgb(var(--color-error-800));
611664
}
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+
}
612749
</style>

0 commit comments

Comments
 (0)