Skip to content

Commit 1ff7564

Browse files
committed
Account Access, Customers
1 parent d54b95d commit 1ff7564

11 files changed

Lines changed: 1908 additions & 81 deletions

File tree

src/lib/config/navigation.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
User,
3+
UserRound,
34
ShieldUser,
45
KeyRound,
56
IdCardLanyard,
@@ -13,6 +14,7 @@ import {
1314
Users,
1415
FileCheck,
1516
Plus,
17+
Building,
1618
Building2,
1719
Eye,
1820
Landmark,
@@ -539,6 +541,37 @@ export function getActiveAbacMenuItem(pathname: string) {
539541
return found || abacItems[0]; // fallback to first item
540542
}
541543

544+
// Customers navigation items
545+
function buildCustomersItems(): NavigationItem[] {
546+
const items: NavigationItem[] = [
547+
{
548+
href: "/customers/individual",
549+
label: "Individual",
550+
iconComponent: UserRound,
551+
},
552+
{
553+
href: "/customers/corporate",
554+
label: "Corporate",
555+
iconComponent: Building,
556+
},
557+
];
558+
559+
return items;
560+
}
561+
562+
export const customersItems = buildCustomersItems();
563+
564+
export function getActiveCustomersMenuItem(pathname: string) {
565+
const found = customersItems.find((item) => {
566+
if (item.external) {
567+
return false;
568+
}
569+
return pathname.startsWith(item.href);
570+
});
571+
572+
return found || customersItems[0];
573+
}
574+
542575
export const navSections: NavigationSection[] = [
543576
{ id: "my-account", label: "My Profile", iconComponent: User, items: myAccountItems, basePaths: ["/user", "/account-access/accounts"] },
544577
{ id: "system", label: "System", iconComponent: Server, items: systemItems, basePaths: ["/system"] },
@@ -550,6 +583,7 @@ export const navSections: NavigationSection[] = [
550583
{ id: "financial-products", label: "Financial Products", iconComponent: Banknote, items: financialProductsItems, basePaths: ["/products/financial", "/products/collections"] },
551584
{ id: "rbac", label: "RBAC", iconComponent: Shield, items: rbacItems, basePaths: ["/rbac"] },
552585
{ id: "banks", label: "Banks", iconComponent: Building2, items: banksItems, basePaths: ["/banks"] },
586+
{ id: "customers", label: "Customers", iconComponent: Users, items: customersItems, basePaths: ["/customers"] },
553587
{ id: "account-access", label: "Account Access", iconComponent: Landmark, items: accountAccessItems, basePaths: ["/account-access"] },
554588
{ id: "dynamic-entities", label: "Dynamic Entities", iconComponent: Box, items: dynamicEntitiesItems, basePaths: ["/dynamic-entities"] },
555589
{ id: "dynamic-endpoints", label: "Dynamic Endpoints", iconComponent: Plug, items: dynamicEndpointsItems, basePaths: ["/dynamic-endpoints"] },

src/lib/utils/roleChecker.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,14 @@ export const SITE_MAP: Record<string, PageRoleConfig> = {
195195
optional: [{ role: "CanExecuteAbacRule" }],
196196
},
197197

198+
// ── Customers ───────────────────────────────────────────
199+
"/customers/individual": {
200+
required: [{ role: "CanGetCustomersAtOneBank", bankScoped: true }],
201+
},
202+
"/customers/corporate": {
203+
required: [{ role: "CanGetCustomersAtOneBank", bankScoped: true }],
204+
},
205+
198206
// ── Users ─────────────────────────────────────────────
199207
"/users": {
200208
required: [{ role: "CanGetAnyUser" }],

src/routes/(protected)/account-access/accounts/+page.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@
8686
<ul class="account-list">
8787
{#each accounts as account}
8888
{@const acctId = account.id || account.account_id}
89-
{@const firstView = account.views_available?.[0]?.id || "owner"}
89+
{@const firstViewObj = account.views_available?.[0]}
90+
{@const firstView = firstViewObj?.id || firstViewObj?.view_id || "owner"}
9091
<li class="account-item">
9192
<a href="/account-access/accounts/{encodeURIComponent(currentBank.bankId)}/{encodeURIComponent(acctId)}/{encodeURIComponent(firstView)}" class="account-link">
9293
<span class="account-id">{acctId}</span>

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

Lines changed: 53 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
return s.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1111
}
1212
13+
/** Handle v6.0.0 views_available using view_id instead of id */
14+
function viewId_(view: any): string {
15+
return view.id || view.view_id || "";
16+
}
17+
1318
let userId = $derived(data.userId || "");
1419
let copied = $state(false);
1520
@@ -124,15 +129,16 @@
124129
125130
const settled = await Promise.allSettled(
126131
views.map(async (view) => {
132+
const vid = viewId_(view);
127133
const res = await trackedFetch(
128-
`/api/obp/banks/${encodeURIComponent(bankId)}/accounts/${encodeURIComponent(accountId)}/views/${encodeURIComponent(view.id)}/users-with-access`
134+
`/api/obp/banks/${encodeURIComponent(bankId)}/accounts/${encodeURIComponent(accountId)}/views/${encodeURIComponent(vid)}/users-with-access`
129135
);
130136
if (!res.ok) {
131137
const data = await res.json().catch(() => ({}));
132-
throw new Error(data.error || `Failed to fetch users with access for view ${view.id}`);
138+
throw new Error(data.error || `Failed to fetch users with access for view ${vid}`);
133139
}
134140
const data = await res.json();
135-
return { viewId: view.id, users: data.users || [] };
141+
return { viewId: vid, users: data.users || [] };
136142
})
137143
);
138144
@@ -142,7 +148,7 @@
142148
143149
for (let i = 0; i < settled.length; i++) {
144150
const result = settled[i];
145-
const viewId = views[i].id;
151+
const viewId = viewId_(views[i]);
146152
if (result.status === "fulfilled") {
147153
anySuccess = true;
148154
const entry = { direct: [] as string[], abac: [] as string[] };
@@ -176,12 +182,13 @@
176182
usersWithAccess = null;
177183
usersByView = new Map();
178184
viewErrors = new Map();
179-
await checkAccountAccess(bankId, accountId, viewId);
180-
if (hasAccountAccess) {
181-
await fetchAccount(bankId, accountId, viewId);
182-
if (account?.views_available?.length) {
183-
await fetchUsersWithAccess(bankId, accountId, account.views_available);
184-
}
185+
// Run access check and account fetch in parallel — access check is informational only
186+
await Promise.all([
187+
checkAccountAccess(bankId, accountId, viewId),
188+
fetchAccount(bankId, accountId, viewId),
189+
]);
190+
if (account?.views_available?.length) {
191+
await fetchUsersWithAccess(bankId, accountId, account.views_available);
185192
}
186193
}
187194
@@ -204,60 +211,7 @@
204211
<span class="breadcrumb-current">{account?.label || accountId}</span>
205212
</nav>
206213

207-
{#if !accessCheckDone}
208-
<div class="loading-state">
209-
<Loader2 size={32} class="spinner-icon" />
210-
<p>Checking account access...</p>
211-
</div>
212-
{:else if hasAccountAccess === false}
213-
<div class="access-warning">
214-
You may need further ABAC Access in order to access this account.
215-
</div>
216-
<details class="debug-panel">
217-
<summary class="debug-summary">
218-
Debug Status
219-
<button class="copy-btn" onclick={(e) => { e.stopPropagation(); copyDebugInfo(); }} title="Copy all debug info">
220-
{#if copied}<Check size={14} />{:else}<Copy size={14} />{/if}
221-
</button>
222-
</summary>
223-
<div class="debug-content">
224-
<div class="debug-grid">
225-
<span class="debug-label">User ID</span>
226-
<span class="debug-value">{userId || ""}</span>
227-
<span class="debug-label">Bank ID</span>
228-
<span class="debug-value">{bankId || ""}</span>
229-
<span class="debug-label">Account ID</span>
230-
<span class="debug-value">{accountId || ""}</span>
231-
<span class="debug-label">View ID</span>
232-
<span class="debug-value">{viewId || ""}</span>
233-
<span class="debug-label">Access Check Done</span>
234-
<span class="debug-value">{accessCheckDone}</span>
235-
<span class="debug-label">Has Account Access</span>
236-
<span class="debug-value debug-false">{String(hasAccountAccess)}</span>
237-
<span class="debug-label">Access Source</span>
238-
<span class="debug-value">{accessSource || ""}</span>
239-
<span class="debug-label">ABAC Rule ID</span>
240-
<span class="debug-value">{abacRuleId || ""}</span>
241-
<span class="debug-label">Has CanExecuteAbacRule</span>
242-
<span class="debug-value" class:debug-true={hasAbacRole} class:debug-false={!hasAbacRole}>{hasAbacRole}</span>
243-
<span class="debug-label">Account Loading</span>
244-
<span class="debug-value">{loading}</span>
245-
<span class="debug-label">Account Loaded</span>
246-
<span class="debug-value" class:debug-true={!!account} class:debug-false={!account && accessCheckDone && hasAccountAccess}>{!!account}</span>
247-
<span class="debug-label">Error</span>
248-
<span class="debug-value" class:debug-false={!!error}>{error || "none"}</span>
249-
<span class="debug-label">User Entitlements</span>
250-
<span class="debug-value">{userEntitlements.length} entitlement{userEntitlements.length !== 1 ? "s" : ""}</span>
251-
</div>
252-
</div>
253-
</details>
254-
{#if !hasAbacRole}
255-
<MissingRoleAlert
256-
roles={["CanExecuteAbacRule"]}
257-
message="You may need this role to gain ABAC access to accounts."
258-
/>
259-
{/if}
260-
{:else if loading}
214+
{#if loading}
261215
<div class="loading-state">
262216
<Loader2 size={32} class="spinner-icon" />
263217
<p>Loading account...</p>
@@ -297,6 +251,11 @@
297251

298252
<!-- Content -->
299253
<div class="panel-content">
254+
{#if accessCheckDone && hasAccountAccess === false}
255+
<div class="access-warning">
256+
You may need further ABAC Access in order to access this account.
257+
</div>
258+
{/if}
300259
<!-- Account Info + Owners row -->
301260
<div class="info-owners-row">
302261
<!-- Basic Info -->
@@ -444,19 +403,28 @@
444403
<div class="views-table">
445404
<div class="views-table-header">
446405
<div class="views-col-name">View</div>
406+
<div class="views-col-link">Transactions</div>
407+
<div class="views-col-link">Counterparties</div>
447408
<div class="views-col-users">Direct Access</div>
448409
<div class="views-col-users">ABAC Access</div>
449410
</div>
450411
{#each account.views_available as view}
451-
{@const viewUsers = usersByView.get(view.id)}
452-
{@const viewError = viewErrors.get(view.id)}
412+
{@const vid = viewId_(view)}
413+
{@const viewUsers = usersByView.get(vid)}
414+
{@const viewError = viewErrors.get(vid)}
453415
<div class="views-table-row">
454416
<div class="views-col-name">
455-
<a href="/account-access/accounts/{encodeURIComponent(bankId)}/{encodeURIComponent(accountId)}/{encodeURIComponent(view.id)}/transactions" class="view-name-link">{toTitleCase(view.id)}</a>
417+
<a href="/account-access/{view.is_system ? 'system-views' : 'custom-views'}/{encodeURIComponent(vid)}" class="view-name-link">{toTitleCase(vid)}</a>
456418
{#if view.is_public}
457419
<span class="view-badge public">PUBLIC</span>
458420
{/if}
459421
</div>
422+
<div class="views-col-link">
423+
<a href="/account-access/accounts/{encodeURIComponent(bankId)}/{encodeURIComponent(accountId)}/{encodeURIComponent(vid)}/transactions" class="view-name-link">Transactions</a>
424+
</div>
425+
<div class="views-col-link">
426+
<a href="/account-access/accounts/{encodeURIComponent(bankId)}/{encodeURIComponent(accountId)}/{encodeURIComponent(vid)}/counterparties" class="view-name-link">Counterparties</a>
427+
</div>
460428
{#if viewError}
461429
<div class="views-col-users view-error" style="grid-column: span 2;">
462430
{viewError}
@@ -1086,7 +1054,7 @@
10861054
10871055
.views-table-header {
10881056
display: grid;
1089-
grid-template-columns: 1fr 1fr 1fr;
1057+
grid-template-columns: 1fr auto auto 1fr 1fr;
10901058
gap: 0;
10911059
background: #f3f4f6;
10921060
border-bottom: 1px solid #e5e7eb;
@@ -1109,7 +1077,7 @@
11091077
11101078
.views-table-row {
11111079
display: grid;
1112-
grid-template-columns: 1fr 1fr 1fr;
1080+
grid-template-columns: 1fr auto auto 1fr 1fr;
11131081
gap: 0;
11141082
border-bottom: 1px solid #e5e7eb;
11151083
}
@@ -1130,6 +1098,12 @@
11301098
flex-wrap: wrap;
11311099
}
11321100
1101+
.views-col-link {
1102+
padding: 0.5rem 0.75rem;
1103+
display: flex;
1104+
align-items: center;
1105+
}
1106+
11331107
.views-col-users {
11341108
padding: 0.5rem 0.75rem;
11351109
display: flex;
@@ -1147,6 +1121,16 @@
11471121
color: var(--color-surface-500);
11481122
}
11491123
1124+
.view-name {
1125+
font-size: 0.875rem;
1126+
font-weight: 600;
1127+
color: #111827;
1128+
}
1129+
1130+
:global([data-mode="dark"]) .view-name {
1131+
color: var(--color-surface-100);
1132+
}
1133+
11501134
.view-name-link {
11511135
font-size: 0.875rem;
11521136
font-weight: 600;

0 commit comments

Comments
 (0)