Four Groups: Navigation Follows Permission, Not Content
The admin sidebar splits into four groups that state permission: your own section for writing, other sections read-only, plus a temporary group.
TL;DR
The admin sidebar was split into four permission-based groups so users can see what they can write to before clicking. The layout is built from one RBAC capability map rather than static lists, keeping read-only sections clearly marked. Server-side authorization still enforces access, and tests confirm the change.
The Modules tab in the KotaPortal admin panel opened the same sidebar since the project began: five sections in one flat list. Wisata, Ekonomi Kreatif, Olahraga, Budaya, and Kepemudaan stood side by side with no sign of which ones the logged-in staff member could actually write to. The client brief on 10 October confirmed a rule that contradicted that layout: every section should be able to read all content, but write only within its own scope. The flat list was not merely a layout untidiness. It masked two different access rights inside one stack of menu items.
The change on 16 October (commit 60dbe36) did not touch routes, endpoints, or authentication logic at all. What changed was how the navigation was arranged: the sidebar was split into four groups, and each group carries a permission meaning the user can read before pressing any link.
Four Groups with Permission Meaning
The first group, Bidang Anda, holds the modules belonging to the user's own section. There is no special marker inside it, because the absence of a marker means write access. The second group contains eight functions whose backend is not yet scoped, and is labeled Fungsi — Lintas (Sementara): a transition status made explicit in front of the user, rather than a hidden link. Once slice 2 work is done, those functions move into Bidang Anda.
The third group, Lintas Bidang — Baca, contains content owned by other sections with a closed-by-default status and an eye icon marking read-only. The fourth group is only active for Semua Bidang. Two communication details accompany the four groups: an identity pill for the section on the desktop sidebar and mobile header, plus a read-only banner that now names the content owner, "Mode baca — konten ini milik bidang Olahraga", instead of the phrase "data bidang lain" that explained nothing.
One Capability Map, Not Four Lists
The four groups are not written as four static lists. The NavItem item receives a readOnly marker, then the buildNavGroups function groups them based on one capability map: BIDANG_MODULE_SLUGS from the RBAC module determines which modules belong to the user's section, and all other modules fall into the read group. The principle matches how React works, rendering different JSX according to conditions [4]; what is needed is a single correct condition as the source.
// Slug menu konten ke users.bidang pemiliknya, khusus copy banner.
// Terpisah dari MODULE_TO_SLUG: arah dan kebutuhan berbeda,
// ini label bidang, bukan slug.
export const SLUG_TO_BIDANG: Record<string, string> = {
wisata: "pariwisata",
"ekonomi-kreatif": "ekraf",
olahraga: "olahraga",
budaya: "budaya",
kepemudaan: "kepemudaan",
};
This second map exists purely for communication: its values are used to write the owning section name onto the read-only banner. The map's name itself mentions slug because it maps in a different direction from MODULE_TO_SLUG, namely labels, not routes.
This separation also makes the navigation friendlier to screen readers. MDN describes nav as a landmark that identifies a large group of links, and when a page carries more than one navigation landmark, each one needs a unique label [2]. Groups with distinct names provide that structure for free. On mobile, the drawer is opened before the menu assertion is tested, while the desktop pill stays attached with display: none on mobile, so a separate helper was written instead of merging with .or().
Why Navigation Must Not Become the Gate
There is one trap that no layout solves. The Next.js authentication guide warns that layouts are not re-rendered when a user navigates between pages, so a session check in the layout does not run on every route change; the check should live close to the data source [1]. OWASP is firm: client-side checks must not be the deciding factor, because client logic is easy to bypass, and permissions must be validated on every request [3]. A tidy navigation is only a hint; the gate stays on the server, and in this case that gate already existed thanks to the open cross-section read rule with a 403 on write.
What makes the four groups trustworthy is the test evidence. Before the menu path test began, the admin login had to be repeated in a loop because a limit of 10 attempts per minute per IP-and-email combination could make consecutive runs fail. The final result: lint without errors, build passed, 206 unit tests passed, module menu test 8/8 on chromium and mobile, and the admin suite at 43/46 with three mobile failures classified as pre-existing.
The remaining decisions close this change with the same principle: status is deliberately left visible. The Sementara group prevents users from wondering why part of the menu suddenly vanished, and prevents the team from deleting links that are merely waiting on the backend. Navigation that is honest about what is not ready yet serves better than a clean but misleading menu.
Sources: