Files
cbsnexora/src/app/platform-admin/menu-access/menu-access.component.scss
T
nitesh 42424dff8b
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
feat(security): tenant-level nav menu access control panel
Adds the frontend half of the per-tenant menu-access ceiling
(TenantMenuAccessApiResource on the backend): a bank cannot show a top-nav
section the platform operator has capped, no matter what any role inside
that tenant grants itself — closing the gap where a role carrying
ALL_FUNCTIONS (as "Clerk" was found to, in two tenants this session)
bypassed every ordinary MENU_.../SIDEBAR_... permission check.

- tenant-menu-access.service.ts: loads the current tenant's list once,
  into a plain Set (not an Observable the directive would have to
  subscribe to, since mifosxHasPermission's setter runs synchronously).
  A failed load is treated as "nothing disabled" so a network hiccup on
  login never hides the whole nav.
- has-permission.directive.ts: checks the tenant's ceiling first, ahead of
  the ALL_FUNCTIONS bypass — that ordering is the actual fix.
- shell.component.ts/html: the nav now waits on the list being loaded
  before rendering at all, so a disabled menu is never shown and then
  hidden a moment later.
- New standalone platform-admin/menu-access screen (#/platform-admin/
  menu-access), reachable without logging in — its own X-Api-Key is the
  real boundary, checked on the backend, not this route. Lets the
  operator pick any tenant and toggle its menu list. Redesigned per
  request: full-width layout, icon-matched toggle tiles (same icon per
  item as the real nav), live enabled-count, connection status.
- Loan Origination and Investment now use their own MENU_LOAN_ORIGINATION
  / MENU_INVESTMENT codes (see backend 0542) instead of sharing
  MENU_ADVANCES / MENU_RBI, so all 27 nav items are independently
  controllable from this screen.

Also: src/assets/env.js — restored 'socity_bank' to
fineractPlatformTenantIds. The tenant now exists in the tenant-store
database; the comment explaining its earlier removal was stale.
2026-09-11 15:13:29 +05:30

432 lines
8.9 KiB
SCSS

/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
// ── Design tokens ──────────────────────────────────────────────────────────
$ink: #0f172a;
$ink-soft: #475569;
$ink-faint: #94a3b8;
$border: #e2e8f0;
$bg: #eef2f7;
$card: #fff;
$accent: #4338ca;
$accent-soft: #eef2ff;
$success: #16a34a;
$success-soft: #f0fdf4;
$danger: #dc2626;
$danger-soft: #fef2f2;
.pma-page {
min-height: 100vh;
width: 100%;
padding: 40px clamp(20px, 4vw, 64px) 80px;
background: radial-gradient(circle at 10% 0%, rgb(67 56 202 / 8%), transparent 45%), $bg;
font-family:
Inter,
system-ui,
-apple-system,
sans-serif;
box-sizing: border-box;
}
.pma-shell {
width: 100%;
}
// ── Header ────────────────────────────────────────────────────────────────
.pma-head {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
&__badge {
flex: none;
width: 46px;
height: 46px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: $ink;
color: #fff;
font-size: 18px;
box-shadow: 0 6px 16px rgb(15 23 42 / 25%);
}
&__title {
margin: 0;
font-size: 21px;
font-weight: 800;
color: $ink;
letter-spacing: -0.01em;
}
&__sub {
margin: 2px 0 0;
font-size: 13px;
color: $ink-soft;
line-height: 1.5;
}
}
// ── Cards ─────────────────────────────────────────────────────────────────
.pma-card {
background: $card;
border: 1px solid $border;
border-radius: 14px;
padding: 24px;
margin-bottom: 16px;
box-shadow: 0 1px 2px rgb(15 23 42 / 4%);
}
// ── Connect block ─────────────────────────────────────────────────────────
.pma-connect__field {
display: flex;
flex-direction: column;
gap: 6px;
label {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: $ink-soft;
}
}
.pma-key-row {
display: flex;
align-items: center;
gap: 10px;
position: relative;
max-width: 640px;
input {
flex: 1;
height: 42px;
padding: 0 14px 0 38px;
border: 1.5px solid $border;
border-radius: 9px;
font-size: 14px;
font-family: ui-monospace, monospace;
color: $ink;
transition: border-color 0.15s;
&:focus {
outline: none;
border-color: $accent;
}
}
}
.pma-key-ic {
position: absolute;
left: 13px;
color: $ink-faint;
font-size: 13px;
pointer-events: none;
}
.pma-status {
display: flex;
align-items: center;
gap: 8px;
margin-top: 14px;
padding: 9px 14px;
border-radius: 8px;
font-size: 12.5px;
font-weight: 600;
&--ok {
background: $success-soft;
color: $success;
}
}
// ── Alerts ────────────────────────────────────────────────────────────────
.pma-alert {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
border-radius: 10px;
font-size: 13px;
font-weight: 500;
margin-bottom: 16px;
&--error {
background: $danger-soft;
color: $danger;
border: 1px solid #fecaca;
}
&--ok {
background: $success-soft;
color: $success;
border: 1px solid #bbf7d0;
}
}
// ── Field / select ────────────────────────────────────────────────────────
.pma-field {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 18px;
label {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: $ink-soft;
}
select {
height: 42px;
max-width: 480px;
padding: 0 14px;
border: 1.5px solid $border;
border-radius: 9px;
font-size: 14px;
font-weight: 600;
color: $ink;
background: #fff;
cursor: pointer;
&:focus {
outline: none;
border-color: $accent;
}
}
}
.pma-loading {
display: flex;
align-items: center;
gap: 10px;
padding: 32px 0;
justify-content: center;
font-size: 13.5px;
color: $ink-soft;
}
// ── Toolbar ───────────────────────────────────────────────────────────────
.pma-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
flex-wrap: wrap;
gap: 10px;
}
.pma-count {
display: flex;
align-items: baseline;
gap: 4px;
&__num {
font-size: 22px;
font-weight: 800;
color: $accent;
}
&__of {
font-size: 12.5px;
font-weight: 600;
color: $ink-faint;
}
}
.pma-toolbar__actions {
display: flex;
gap: 8px;
}
// ── Buttons ───────────────────────────────────────────────────────────────
.pma-btn {
display: inline-flex;
align-items: center;
gap: 8px;
height: 40px;
padding: 0 18px;
border-radius: 8px;
font-size: 13px;
font-weight: 700;
cursor: pointer;
border: 1.5px solid transparent;
font-family: inherit;
transition: all 0.15s;
white-space: nowrap;
&--primary {
background: $ink;
color: #fff;
&:hover:not(:disabled) {
background: #1e293b;
}
&:disabled {
opacity: 0.55;
cursor: not-allowed;
}
}
&--ghost {
background: #fff;
border-color: $border;
color: $ink-soft;
height: 34px;
padding: 0 13px;
font-size: 12.5px;
&:hover {
border-color: $accent;
color: $accent;
background: $accent-soft;
}
}
}
// ── Menu tile grid ────────────────────────────────────────────────────────
.pma-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 10px;
}
.pma-tile {
display: flex;
align-items: center;
gap: 11px;
padding: 12px 14px;
border: 1.5px solid $border;
border-radius: 10px;
background: #fafbfc;
cursor: pointer;
transition: all 0.15s;
&:hover {
border-color: #cbd5e1;
}
&--on {
background: $accent-soft;
border-color: #c7d2fe;
}
}
.pma-tile__ic {
flex: none;
width: 20px;
text-align: center;
font-size: 14px;
color: $ink-faint;
.pma-tile--on & {
color: $accent;
}
}
.pma-tile__label {
flex: 1;
font-size: 13.5px;
font-weight: 600;
color: $ink;
line-height: 1.25;
}
// Custom toggle switch — checkbox visually hidden, styled track+thumb driven by :checked.
.pma-switch {
position: relative;
flex: none;
width: 36px;
height: 20px;
input {
position: absolute;
inset: 0;
opacity: 0;
margin: 0;
cursor: pointer;
z-index: 1;
&:checked ~ .pma-switch__track {
background: $accent;
.pma-switch__thumb {
transform: translateX(16px);
}
}
}
}
.pma-switch__track {
position: absolute;
inset: 0;
border-radius: 999px;
background: #cbd5e1;
transition: background 0.15s;
}
.pma-switch__thumb {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 3px rgb(0 0 0 / 25%);
transition: transform 0.15s;
}
// ── Save bar ──────────────────────────────────────────────────────────────
.pma-save-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-top: 20px;
padding-top: 18px;
border-top: 1px solid $border;
flex-wrap: wrap;
}
.pma-save-bar__hint {
font-size: 12px;
color: $ink-faint;
}
.pma-save {
margin-left: auto;
}
// ── Responsive ────────────────────────────────────────────────────────────
@media (width <= 560px) {
.pma-key-row {
flex-wrap: wrap;
}
.pma-grid {
grid-template-columns: 1fr;
}
.pma-save-bar {
flex-direction: column;
align-items: stretch;
}
.pma-save {
margin-left: 0;
justify-content: center;
}
}