跪拜 Guibai
← All articles
Vue.js

The Two Permission Bugs That Screenshots Caught: Button-Level Trimming and a 403 Dressed as 404

By ruanno_top ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Splitting menu-level and button-level permissions, and distinguishing 403 from 404, turns confusing “page gone” support tickets into clear “you lack access” messages. The same screenshot-as-evidence acceptance method is directly reusable in any role-based admin project.

Summary

This admin backend builds its entire navigation from a single `menuTree`: it filters the tree by role to produce the sidebar, then uses the same filtered result to register dynamic routes via `router.addRoute`. The stated benefit is that adding a page touches only two places, and unauthorized pages are never registered at all rather than being hidden.

The project separates roles from permission codes. Roles decide which menus appear; permission codes decide which buttons click. The first bug hit exactly there: the user list page never applied the `v-permission` directive, so a product role with no delete permission still saw every row's delete button. The fix attaches permission codes to each action button, and the directive removes unauthorized elements from the DOM entirely.

The second bug came from the guard logic. Because unauthorized routes aren't registered, manually visiting a path like `/system/roles` resolved to the catch-all NotFound, rendering a 404. The guard now checks whether the requested path exists in the unfiltered menu path set: if it exists but the role lacks access, it redirects to `/403`; only truly unknown paths get a 404.

Beyond permissions, the post covers multi-tab caching driven by `keep-alive`'s `include` list (derived from open tabs, never hand-written), theme switching via a single `data-rn-theme` attribute backed by CSS tokens, and a zero-dependency SVG chart set. The author's core workflow lesson is to accept admin features by logging in as each role, screenshotting every step, and asserting on the DOM, because code review and docs both miss execution-layer gaps.

Takeaways
— Roles and permission codes are separate: roles determine visible menus, permission codes determine enabled buttons; merging them produces over- or under-restricted UI.
— A single `menuTree` with `filterMenuTree` and `buildRouteRecords` generates the sidebar and dynamic routes from the same source, so unauthorized routes are never registered.
— After `router.addRoute`, the guard must return `{ path: to.fullPath }` rather than spreading `to`, because `to.name` already resolves to the catch-all `not-found` and would re-trigger 404.
— The `v-permission` directive removes unauthorized elements from the DOM (not `display:none`) to prevent script-triggered clicks.
— The user management view missed `v-permission`, so a product role saw delete buttons; the fix added permission codes to create, batch delete, view, edit, and delete actions.
— To distinguish 403 from 404, collect all menu paths without role filtering; a path in that set that the role can't access redirects to 403, while unknown paths remain 404.
— `keep-alive`'s `include` list must be computed from currently open tabs that declare `keepAlive`, not hard-coded.
— Theme switching works by setting `data-rn-theme` on the document root; colors come solely from CSS `--rn-*` tokens, so no component needs dark-mode branches.
— Frontend permission checks are UX, not security; server-side validation by permission codes is mandatory.
— Acceptance should be: switch roles to check menus, type unauthorized addresses to see 403, inspect buttons row by row, close tabs to verify cache, and switch themes to catch white patches.
Conclusions

Screenshotting every role in a real browser is a cheap, high-signal acceptance method: the post proves it caught two defects that code reading and documentation would have missed.

The two bugs share a common failure mode: the permission infrastructure (codes, 403 page) existed, but an execution layer (the user view, the guard redirect) never consumed it. In practice, this pattern is more likely than missing infrastructure.

Hiding a button with `v-permission` but leaving the element in the DOM would be an invitation to scripted clicks; removing it entirely is the right call, even though it complicates debugging.

The author's insistence on one `menuTree` for menus and routes trades a little flexibility for a big win: the system can't drift into a menu that points to a route that doesn't exist.

The 403 vs 404 distinction is a small change with outsized user experience impact—it converts ambiguous “feature missing” reports into clear “you don't have access” feedback, cutting support noise.

Concepts & terms
v-permission directive
A Vue custom directive that checks a user's permission codes and removes the bound element from the DOM if the code is missing, instead of merely hiding it with CSS.
Catch-all route
A route pattern that matches any undefined path, typically used to render a 404 NotFound page. Dynamic routes intercept the catch-all unless the guard re-matches explicitly.
keep-alive include
Vue's keep-alive component accepts an `include` prop listing component names to cache. In this admin, the list is computed from open tabs that declare `keepAlive`, preserving table filters and scroll position when switching tabs.
menuTree
A single declarative tree where each node holds path, component key, title key, icon, allowed roles, and keepAlive/affix flags. It serves as the sole source for generating the sidebar menu, dynamic routes, and breadcrumbs.
Permission code
A string like `system:user:delete` that represents a specific action. Roles map to arrays of these codes, and UI elements check membership to decide visibility or behavior.
From the discussion

The article's premise is challenged as outdated, while another position defends the acceptance checklist as necessary because AI-generated code passes tests yet misses permission bugs that only role-switching screenshot verification catch.

— AI can write code but not guarantee correct behavior; passing tests don't catch permission bugs that only appear under real role-switched flows.
— The acceptance method of role-switch login plus DOM-assertion screenshots is a practical safeguard against AI blind spots.
— The topic is considered stale in 2026, raising doubt about its continued relevance for current development practice.
Featured comments
ruanno_top

It can write it, but writing it correctly is another matter. These two bugs are exactly what AI wrote itself, and even with tests all green they weren't caught; only real-device role-switching screenshots exposed them. So now I treat 'role-switch login + DOM-assertion screenshots' as an acceptance step, not relying on its self-check.

前端小豆

It's 2026; posting this now is meaningless.

See top comments, translated →
Source: juejin.cn ↗ Google Translate ↗ Backup ↗