Skip to content

Commit d54b95d

Browse files
committed
Search user by Role. Account Access users with access
1 parent 3aa5a1d commit d54b95d

File tree

4 files changed

+189
-66
lines changed

4 files changed

+189
-66
lines changed

src/routes/(protected)/account-access/accounts/[bank_id]/[account_id]/[view_id]/+page.svelte

Lines changed: 96 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -58,27 +58,9 @@
5858
return { type: "general" as const, message: usersWithAccessError };
5959
});
6060
61-
// Group users by view_id and access_source for display in Views Available
62-
let usersByView = $derived.by(() => {
63-
if (!usersWithAccess?.users) return new Map<string, { direct: string[]; abac: string[] }>();
64-
const map = new Map<string, { direct: string[]; abac: string[] }>();
65-
for (const user of usersWithAccess.users) {
66-
if (!user.views) continue;
67-
for (const view of user.views) {
68-
if (!map.has(view.view_id)) {
69-
map.set(view.view_id, { direct: [], abac: [] });
70-
}
71-
const entry = map.get(view.view_id)!;
72-
const name = user.username || user.user_id || "Unknown";
73-
if (view.access_source === "ABAC") {
74-
entry.abac.push(name);
75-
} else {
76-
entry.direct.push(name);
77-
}
78-
}
79-
}
80-
return map;
81-
});
61+
// Users grouped by view_id for display in Views Available
62+
let usersByView = $state(new Map<string, { direct: string[]; abac: string[] }>());
63+
let viewErrors = $state(new Map<string, string>());
8264
8365
let bankId = $derived(page.params.bank_id || "");
8466
let accountId = $derived(page.params.account_id || "");
@@ -134,36 +116,72 @@
134116
}
135117
}
136118
137-
async function fetchUsersWithAccess(bankId: string, accountId: string) {
119+
async function fetchUsersWithAccess(bankId: string, accountId: string, views: any[]) {
138120
usersWithAccessLoading = true;
139121
usersWithAccessError = null;
140-
try {
141-
const res = await trackedFetch(
142-
`/api/obp/banks/${encodeURIComponent(bankId)}/accounts/${encodeURIComponent(accountId)}/users-with-access`
143-
);
144-
if (!res.ok) {
145-
const data = await res.json().catch(() => ({}));
146-
throw new Error(data.error || "Failed to fetch users with account access");
122+
usersByView = new Map();
123+
viewErrors = new Map();
124+
125+
const settled = await Promise.allSettled(
126+
views.map(async (view) => {
127+
const res = await trackedFetch(
128+
`/api/obp/banks/${encodeURIComponent(bankId)}/accounts/${encodeURIComponent(accountId)}/views/${encodeURIComponent(view.id)}/users-with-access`
129+
);
130+
if (!res.ok) {
131+
const data = await res.json().catch(() => ({}));
132+
throw new Error(data.error || `Failed to fetch users with access for view ${view.id}`);
133+
}
134+
const data = await res.json();
135+
return { viewId: view.id, users: data.users || [] };
136+
})
137+
);
138+
139+
const map = new Map<string, { direct: string[]; abac: string[] }>();
140+
const errors = new Map<string, string>();
141+
let anySuccess = false;
142+
143+
for (let i = 0; i < settled.length; i++) {
144+
const result = settled[i];
145+
const viewId = views[i].id;
146+
if (result.status === "fulfilled") {
147+
anySuccess = true;
148+
const entry = { direct: [] as string[], abac: [] as string[] };
149+
for (const user of result.value.users) {
150+
const name = user.username || user.user_id || "Unknown";
151+
if (user.access_source === "ABAC") {
152+
entry.abac.push(name);
153+
} else {
154+
entry.direct.push(name);
155+
}
156+
}
157+
map.set(viewId, entry);
158+
} else {
159+
const msg = result.reason instanceof Error ? result.reason.message : String(result.reason);
160+
errors.set(viewId, msg);
147161
}
148-
usersWithAccess = await res.json();
149-
} catch (err) {
150-
usersWithAccessError = err instanceof Error ? err.message : "Failed to fetch users with account access";
151-
usersWithAccess = null;
152-
} finally {
153-
usersWithAccessLoading = false;
154162
}
163+
164+
usersByView = map;
165+
viewErrors = errors;
166+
usersWithAccess = anySuccess ? { users: [] } : null;
167+
if (errors.size > 0 && !anySuccess) {
168+
usersWithAccessError = "Failed to fetch users with access for all views";
169+
}
170+
usersWithAccessLoading = false;
155171
}
156172
157173
async function loadPage(bankId: string, accountId: string, viewId: string) {
158174
account = null;
159175
error = null;
160176
usersWithAccess = null;
177+
usersByView = new Map();
178+
viewErrors = new Map();
161179
await checkAccountAccess(bankId, accountId, viewId);
162180
if (hasAccountAccess) {
163-
await Promise.all([
164-
fetchAccount(bankId, accountId, viewId),
165-
fetchUsersWithAccess(bankId, accountId)
166-
]);
181+
await fetchAccount(bankId, accountId, viewId);
182+
if (account?.views_available?.length) {
183+
await fetchUsersWithAccess(bankId, accountId, account.views_available);
184+
}
167185
}
168186
}
169187
@@ -431,35 +449,42 @@
431449
</div>
432450
{#each account.views_available as view}
433451
{@const viewUsers = usersByView.get(view.id)}
452+
{@const viewError = viewErrors.get(view.id)}
434453
<div class="views-table-row">
435454
<div class="views-col-name">
436455
<a href="/account-access/accounts/{encodeURIComponent(bankId)}/{encodeURIComponent(accountId)}/{encodeURIComponent(view.id)}/transactions" class="view-name-link">{toTitleCase(view.id)}</a>
437456
{#if view.is_public}
438457
<span class="view-badge public">PUBLIC</span>
439458
{/if}
440459
</div>
441-
<div class="views-col-users">
442-
{#if usersWithAccessLoading}
443-
<Loader2 size={14} class="spinner-icon" />
444-
{:else if viewUsers?.direct?.length}
445-
{#each viewUsers.direct as username}
446-
<span class="user-chip direct">{username}</span>
447-
{/each}
448-
{:else if !usersWithAccessError}
449-
<span class="no-users">—</span>
450-
{/if}
451-
</div>
452-
<div class="views-col-users">
453-
{#if usersWithAccessLoading}
454-
<Loader2 size={14} class="spinner-icon" />
455-
{:else if viewUsers?.abac?.length}
456-
{#each viewUsers.abac as username}
457-
<span class="user-chip abac">{username}</span>
458-
{/each}
459-
{:else if !usersWithAccessError}
460-
<span class="no-users">—</span>
461-
{/if}
462-
</div>
460+
{#if viewError}
461+
<div class="views-col-users view-error" style="grid-column: span 2;">
462+
{viewError}
463+
</div>
464+
{:else}
465+
<div class="views-col-users">
466+
{#if usersWithAccessLoading}
467+
<Loader2 size={14} class="spinner-icon" />
468+
{:else if viewUsers?.direct?.length}
469+
{#each viewUsers.direct as username}
470+
<span class="user-chip direct">{username}</span>
471+
{/each}
472+
{:else}
473+
<span class="no-users">—</span>
474+
{/if}
475+
</div>
476+
<div class="views-col-users">
477+
{#if usersWithAccessLoading}
478+
<Loader2 size={14} class="spinner-icon" />
479+
{:else if viewUsers?.abac?.length}
480+
{#each viewUsers.abac as username}
481+
<span class="user-chip abac">{username}</span>
482+
{/each}
483+
{:else}
484+
<span class="no-users">—</span>
485+
{/if}
486+
</div>
487+
{/if}
463488
</div>
464489
{/each}
465490
</div>
@@ -1183,6 +1208,16 @@
11831208
color: rgb(var(--color-warning-300));
11841209
}
11851210
1211+
.view-error {
1212+
font-size: 0.75rem;
1213+
color: #dc2626;
1214+
padding: 0.5rem 0.75rem;
1215+
}
1216+
1217+
:global([data-mode="dark"]) .view-error {
1218+
color: rgb(var(--color-error-400));
1219+
}
1220+
11861221
.no-users {
11871222
color: #d1d5db;
11881223
font-size: 0.875rem;

src/routes/(protected)/users/+page.server.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,14 @@ export const load: PageServerLoad = async ({ locals, url }) => {
4444
// Fetch users from OBP API - get recent users with pagination
4545
logger.info("=== USERS API CALL ===");
4646
const role = url.searchParams.get("role_name");
47+
const bankId = url.searchParams.get("bank_id");
4748
let endpoint = `/obp/v6.0.0/users?limit=100`;
4849
if (role) {
4950
endpoint += `&role_name=${encodeURIComponent(role)}`;
5051
}
52+
if (bankId) {
53+
endpoint += `&bank_id=${encodeURIComponent(bankId)}`;
54+
}
5155
logger.info(`Request: ${endpoint}`);
5256

5357
const response = await obp_requests.get(endpoint, accessToken);

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

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
let { data } = $props<{ data: PageData }>();
88
99
let roleFilter = $state(page.url.searchParams.get("role_name") || "");
10+
let bankIdFilter = $state(page.url.searchParams.get("bank_id") || "");
1011
let roles = $state<string[]>([]);
12+
let banks = $state<string[]>([]);
1113
12-
// Fetch roles on mount
14+
// Fetch roles and banks on mount
1315
$effect(() => {
1416
async function fetchRoles() {
1517
try {
@@ -22,7 +24,19 @@
2224
console.error("Error fetching roles:", err);
2325
}
2426
}
27+
async function fetchBanks() {
28+
try {
29+
const response = await fetch("/api/banks");
30+
const result = await response.json();
31+
if (result.banks) {
32+
banks = result.banks.map((b: any) => b.id).sort();
33+
}
34+
} catch (err) {
35+
console.error("Error fetching banks:", err);
36+
}
37+
}
2538
fetchRoles();
39+
fetchBanks();
2640
});
2741
2842
let users = $derived(data.users || []);
@@ -245,6 +259,9 @@
245259
if (roleFilter.trim()) {
246260
params.set("role_name", roleFilter.trim());
247261
}
262+
if (bankIdFilter.trim()) {
263+
params.set("bank_id", bankIdFilter.trim());
264+
}
248265
const qs = params.toString();
249266
goto(qs ? `?${qs}` : "/users", { invalidateAll: true });
250267
}}
@@ -265,13 +282,28 @@
265282
{/each}
266283
</select>
267284
</div>
285+
<div style="flex: 0 0 300px;">
286+
<label for="bank-id-input" class="block text-sm font-medium mb-2"
287+
>Bank ID</label
288+
>
289+
<select
290+
id="bank-id-input"
291+
bind:value={bankIdFilter}
292+
class="form-input w-full"
293+
>
294+
<option value="">All banks</option>
295+
{#each banks as bank}
296+
<option value={bank}>{bank}</option>
297+
{/each}
298+
</select>
299+
</div>
268300
<button
269301
type="submit"
270302
class="btn btn-primary"
271303
>
272-
Filter by Role
304+
Filter
273305
</button>
274-
{#if page.url.searchParams.has("role_name")}
306+
{#if page.url.searchParams.has("role_name") || page.url.searchParams.has("bank_id")}
275307
<a href="/users" class="btn btn-secondary">Clear</a>
276308
{/if}
277309
</form>
@@ -334,8 +366,8 @@
334366
<!-- Users List Panel -->
335367
<div class="panel">
336368
<div class="panel-header">
337-
<h2 class="panel-title">{page.url.searchParams.has("role_name") ? `Users with Role: ${page.url.searchParams.get("role_name")}` : "Recent Users"}</h2>
338-
<div class="panel-subtitle">{page.url.searchParams.has("role_name") ? `Filtered by role` : "Most recently created users"} (up to 100)</div>
369+
<h2 class="panel-title">{page.url.searchParams.has("role_name") || page.url.searchParams.has("bank_id") ? `Users${page.url.searchParams.has("role_name") ? ` with Role: ${page.url.searchParams.get("role_name")}` : ""}${page.url.searchParams.has("bank_id") ? ` at Bank: ${page.url.searchParams.get("bank_id")}` : ""}` : "Recent Users"}</h2>
370+
<div class="panel-subtitle">{page.url.searchParams.has("role_name") || page.url.searchParams.has("bank_id") ? "Filtered results" : "Most recently created users"} (up to 100)</div>
339371
</div>
340372
<div class="panel-content">
341373
{#if users && users.length > 0}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { json } from "@sveltejs/kit";
2+
import type { RequestHandler } from "./$types";
3+
import { obp_requests } from "$lib/obp/requests";
4+
import { extractErrorDetails } from "$lib/obp/errors";
5+
import { SessionOAuthHelper } from "$lib/oauth/sessionHelper";
6+
import { createLogger } from "$lib/utils/logger";
7+
8+
const logger = createLogger("UsersWithViewAccessAPI");
9+
10+
export const GET: RequestHandler = async ({ locals, params }) => {
11+
const session = locals.session;
12+
13+
if (!session?.data?.user) {
14+
return json({ error: "Unauthorized" }, { status: 401 });
15+
}
16+
17+
const sessionOAuth = SessionOAuthHelper.getSessionOAuth(session);
18+
const accessToken = sessionOAuth?.accessToken;
19+
20+
if (!accessToken) {
21+
logger.warn("No access token available for fetching users with view access");
22+
return json({ error: "No API access token available" }, { status: 401 });
23+
}
24+
25+
const { bank_id, account_id, view_id } = params;
26+
27+
if (!bank_id || !account_id || !view_id) {
28+
return json({ error: "Bank ID, Account ID and View ID are required" }, { status: 400 });
29+
}
30+
31+
try {
32+
logger.info(`Fetching users with access for bank: ${bank_id}, account: ${account_id}, view: ${view_id}`);
33+
34+
const endpoint = `/obp/v6.0.0/banks/${encodeURIComponent(bank_id)}/accounts/${encodeURIComponent(account_id)}/views/${encodeURIComponent(view_id)}/users-with-access`;
35+
const response = await obp_requests.get(endpoint, accessToken);
36+
37+
logger.info(`Retrieved users with access for view ${view_id}`);
38+
39+
return json(response, { status: 200 });
40+
} catch (err) {
41+
logger.error(`Error fetching users with access for view ${view_id}:`, err);
42+
43+
const { message, obpErrorCode } = extractErrorDetails(err);
44+
45+
const errorResponse: any = { error: message };
46+
if (obpErrorCode) {
47+
errorResponse.obpErrorCode = obpErrorCode;
48+
}
49+
50+
return json(errorResponse, { status: 500 });
51+
}
52+
};

0 commit comments

Comments
 (0)