Skip to content

Commit 7384484

Browse files
committed
Showing Users that have access.
1 parent dcd40e2 commit 7384484

File tree

2 files changed

+282
-30
lines changed
  • src/routes
    • (protected)/account-access/accounts/[bank_id]/[account_id]/[view_id]
    • api/obp/banks/[bank_id]/accounts/[account_id]/users-with-access

2 files changed

+282
-30
lines changed

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

Lines changed: 230 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,44 @@
3838
let abacRuleId = $state<string>("");
3939
let accessCheckDone = $state(false);
4040
41+
let usersWithAccess = $state<any>(null);
42+
let usersWithAccessLoading = $state(false);
43+
let usersWithAccessError = $state<string | null>(null);
44+
45+
let usersWithAccessParsedError = $derived.by(() => {
46+
if (!usersWithAccessError) return null;
47+
const missingRoleMatch = usersWithAccessError.match(
48+
/OBP-(\d+):.*missing one or more roles:\s*(.+)/i,
49+
);
50+
if (missingRoleMatch) {
51+
const roles = missingRoleMatch[2].split(",").map((r: string) => r.trim());
52+
return { type: "missing_role" as const, code: missingRoleMatch[1], roles, message: usersWithAccessError };
53+
}
54+
return { type: "general" as const, message: usersWithAccessError };
55+
});
56+
57+
// Group users by view_id and access_source for display in Views Available
58+
let usersByView = $derived.by(() => {
59+
if (!usersWithAccess?.users) return new Map<string, { direct: string[]; abac: string[] }>();
60+
const map = new Map<string, { direct: string[]; abac: string[] }>();
61+
for (const user of usersWithAccess.users) {
62+
if (!user.views) continue;
63+
for (const view of user.views) {
64+
if (!map.has(view.view_id)) {
65+
map.set(view.view_id, { direct: [], abac: [] });
66+
}
67+
const entry = map.get(view.view_id)!;
68+
const name = user.username || user.user_id || "Unknown";
69+
if (view.access_source === "ABAC") {
70+
entry.abac.push(name);
71+
} else {
72+
entry.direct.push(name);
73+
}
74+
}
75+
}
76+
return map;
77+
});
78+
4179
let bankId = $derived(page.params.bank_id);
4280
let accountId = $derived(page.params.account_id);
4381
let viewId = $derived(page.params.view_id);
@@ -92,12 +130,36 @@
92130
}
93131
}
94132
133+
async function fetchUsersWithAccess(bankId: string, accountId: string) {
134+
usersWithAccessLoading = true;
135+
usersWithAccessError = null;
136+
try {
137+
const res = await trackedFetch(
138+
`/api/obp/banks/${encodeURIComponent(bankId)}/accounts/${encodeURIComponent(accountId)}/users-with-access`
139+
);
140+
if (!res.ok) {
141+
const data = await res.json().catch(() => ({}));
142+
throw new Error(data.error || "Failed to fetch users with account access");
143+
}
144+
usersWithAccess = await res.json();
145+
} catch (err) {
146+
usersWithAccessError = err instanceof Error ? err.message : "Failed to fetch users with account access";
147+
usersWithAccess = null;
148+
} finally {
149+
usersWithAccessLoading = false;
150+
}
151+
}
152+
95153
async function loadPage(bankId: string, accountId: string, viewId: string) {
96154
account = null;
97155
error = null;
156+
usersWithAccess = null;
98157
await checkAccountAccess(bankId, accountId, viewId);
99158
if (hasAccountAccess) {
100-
await fetchAccount(bankId, accountId, viewId);
159+
await Promise.all([
160+
fetchAccount(bankId, accountId, viewId),
161+
fetchUsersWithAccess(bankId, accountId)
162+
]);
101163
}
102164
}
103165
@@ -350,16 +412,60 @@
350412
<h2 class="section-title">
351413
Views Available ({account.views_available.length})
352414
</h2>
353-
<div class="views-list">
415+
{#if usersWithAccessError}
416+
{#if usersWithAccessParsedError && usersWithAccessParsedError.type === "missing_role"}
417+
<MissingRoleAlert
418+
roles={usersWithAccessParsedError.roles}
419+
errorCode={usersWithAccessParsedError.code}
420+
message={usersWithAccessParsedError.message}
421+
bankId={bankId}
422+
/>
423+
{:else}
424+
<div class="users-access-error">
425+
{usersWithAccessError}
426+
</div>
427+
{/if}
428+
{/if}
429+
<div class="views-table">
430+
<div class="views-table-header">
431+
<div class="views-col-name">View</div>
432+
<div class="views-col-users">Direct</div>
433+
<div class="views-col-users">ABAC</div>
434+
</div>
354435
{#each account.views_available as view}
355-
<div class="view-item">
356-
<span class="view-name">{view.short_name || view.id}</span>
357-
{#if view.description}
358-
<span class="view-description">{view.description}</span>
359-
{/if}
360-
{#if view.is_public}
361-
<span class="view-badge public">PUBLIC</span>
362-
{/if}
436+
{@const viewUsers = usersByView.get(view.id)}
437+
<div class="views-table-row">
438+
<div class="views-col-name">
439+
<span class="view-name">{view.short_name || view.id}</span>
440+
{#if view.description}
441+
<span class="view-description">{view.description}</span>
442+
{/if}
443+
{#if view.is_public}
444+
<span class="view-badge public">PUBLIC</span>
445+
{/if}
446+
</div>
447+
<div class="views-col-users">
448+
{#if usersWithAccessLoading}
449+
<Loader2 size={14} class="spinner-icon" />
450+
{:else if viewUsers?.direct?.length}
451+
{#each viewUsers.direct as username}
452+
<span class="user-chip direct">{username}</span>
453+
{/each}
454+
{:else if !usersWithAccessError}
455+
<span class="no-users">—</span>
456+
{/if}
457+
</div>
458+
<div class="views-col-users">
459+
{#if usersWithAccessLoading}
460+
<Loader2 size={14} class="spinner-icon" />
461+
{:else if viewUsers?.abac?.length}
462+
{#each viewUsers.abac as username}
463+
<span class="user-chip abac">{username}</span>
464+
{/each}
465+
{:else if !usersWithAccessError}
466+
<span class="no-users">—</span>
467+
{/if}
468+
</div>
363469
</div>
364470
{/each}
365471
</div>
@@ -810,6 +916,23 @@
810916
color: var(--color-surface-300);
811917
}
812918
919+
/* Users with access error */
920+
.users-access-error {
921+
padding: 0.75rem 1rem;
922+
margin-bottom: 1rem;
923+
background: #fef2f2;
924+
border: 1px solid #fca5a5;
925+
border-radius: 6px;
926+
color: #991b1b;
927+
font-size: 0.875rem;
928+
}
929+
930+
:global([data-mode="dark"]) .users-access-error {
931+
background: rgba(220, 38, 38, 0.1);
932+
border-color: rgba(220, 38, 38, 0.3);
933+
color: rgb(var(--color-error-300));
934+
}
935+
813936
/* Routings */
814937
.routings-list {
815938
display: flex;
@@ -935,26 +1058,78 @@
9351058
color: rgb(var(--color-warning-300));
9361059
}
9371060
938-
/* Views */
939-
.views-list {
1061+
/* Views table */
1062+
.views-table {
1063+
border: 1px solid #e5e7eb;
1064+
border-radius: 6px;
1065+
overflow: hidden;
1066+
}
1067+
1068+
:global([data-mode="dark"]) .views-table {
1069+
border-color: rgb(var(--color-surface-700));
1070+
}
1071+
1072+
.views-table-header {
1073+
display: grid;
1074+
grid-template-columns: 1fr 1fr 1fr;
1075+
gap: 0;
1076+
background: #f3f4f6;
1077+
border-bottom: 1px solid #e5e7eb;
1078+
font-size: 0.75rem;
1079+
font-weight: 600;
1080+
color: #6b7280;
1081+
text-transform: uppercase;
1082+
letter-spacing: 0.05em;
1083+
}
1084+
1085+
:global([data-mode="dark"]) .views-table-header {
1086+
background: rgb(var(--color-surface-900));
1087+
border-bottom-color: rgb(var(--color-surface-700));
1088+
color: var(--color-surface-400);
1089+
}
1090+
1091+
.views-table-header > div {
1092+
padding: 0.625rem 1rem;
1093+
}
1094+
1095+
.views-table-row {
1096+
display: grid;
1097+
grid-template-columns: 1fr 1fr 1fr;
1098+
gap: 0;
1099+
border-bottom: 1px solid #e5e7eb;
1100+
}
1101+
1102+
.views-table-row:last-child {
1103+
border-bottom: none;
1104+
}
1105+
1106+
:global([data-mode="dark"]) .views-table-row {
1107+
border-bottom-color: rgb(var(--color-surface-700));
1108+
}
1109+
1110+
.views-col-name {
1111+
padding: 0.75rem 1rem;
9401112
display: flex;
941-
flex-direction: column;
1113+
align-items: center;
9421114
gap: 0.5rem;
1115+
flex-wrap: wrap;
9431116
}
9441117
945-
.view-item {
1118+
.views-col-users {
1119+
padding: 0.75rem 1rem;
9461120
display: flex;
9471121
align-items: center;
948-
gap: 0.75rem;
949-
padding: 0.75rem 1rem;
950-
background: #fafafa;
951-
border: 1px solid #e5e7eb;
952-
border-radius: 6px;
1122+
flex-wrap: wrap;
1123+
gap: 0.375rem;
9531124
}
9541125
955-
:global([data-mode="dark"]) .view-item {
956-
background: rgb(var(--color-surface-900));
957-
border-color: rgb(var(--color-surface-700));
1126+
.views-col-users :global(.spinner-icon) {
1127+
animation: spin 1s linear infinite;
1128+
color: #9ca3af;
1129+
}
1130+
1131+
:global([data-mode="dark"]) .views-col-users :global(.spinner-icon) {
1132+
color: var(--color-surface-500);
9581133
}
9591134
9601135
.view-name {
@@ -970,7 +1145,6 @@
9701145
.view-description {
9711146
font-size: 0.8rem;
9721147
color: #6b7280;
973-
flex: 1;
9741148
}
9751149
9761150
:global([data-mode="dark"]) .view-description {
@@ -982,7 +1156,6 @@
9821156
padding: 0.125rem 0.5rem;
9831157
border-radius: 9999px;
9841158
font-weight: 600;
985-
margin-left: auto;
9861159
}
9871160
9881161
.view-badge.public {
@@ -995,14 +1168,41 @@
9951168
color: rgb(var(--color-success-300));
9961169
}
9971170
998-
.view-badge.private {
999-
background: #fee2e2;
1000-
color: #991b1b;
1171+
.user-chip {
1172+
display: inline-block;
1173+
font-size: 0.75rem;
1174+
padding: 0.15rem 0.5rem;
1175+
border-radius: 4px;
1176+
font-weight: 500;
10011177
}
10021178
1003-
:global([data-mode="dark"]) .view-badge.private {
1004-
background: rgba(239, 68, 68, 0.2);
1005-
color: rgb(var(--color-error-300));
1179+
.user-chip.direct {
1180+
background: #dbeafe;
1181+
color: #1e40af;
1182+
}
1183+
1184+
:global([data-mode="dark"]) .user-chip.direct {
1185+
background: rgba(59, 130, 246, 0.2);
1186+
color: rgb(var(--color-primary-300));
1187+
}
1188+
1189+
.user-chip.abac {
1190+
background: #fef3c7;
1191+
color: #92400e;
1192+
}
1193+
1194+
:global([data-mode="dark"]) .user-chip.abac {
1195+
background: rgba(245, 158, 11, 0.2);
1196+
color: rgb(var(--color-warning-300));
1197+
}
1198+
1199+
.no-users {
1200+
color: #d1d5db;
1201+
font-size: 0.875rem;
1202+
}
1203+
1204+
:global([data-mode="dark"]) .no-users {
1205+
color: var(--color-surface-600);
10061206
}
10071207
10081208
/* Empty / Loading / Error states */
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("UsersWithAccountAccessAPI");
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 account access");
22+
return json({ error: "No API access token available" }, { status: 401 });
23+
}
24+
25+
const { bank_id, account_id } = params;
26+
27+
if (!bank_id || !account_id) {
28+
return json({ error: "Bank ID and Account ID are required" }, { status: 400 });
29+
}
30+
31+
try {
32+
logger.info(`Fetching users with access for bank: ${bank_id}, account: ${account_id}`);
33+
34+
const endpoint = `/obp/v6.0.0/banks/${encodeURIComponent(bank_id)}/accounts/${encodeURIComponent(account_id)}/users-with-access`;
35+
const response = await obp_requests.get(endpoint, accessToken);
36+
37+
logger.info(`Retrieved users with access for account ${account_id}`);
38+
39+
return json(response, { status: 200 });
40+
} catch (err) {
41+
logger.error("Error fetching users with account access:", 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)