Skip to content

View-Level Gating

Our server-side Express routes are now defensively secured with the requireRole middleware. But the resulting user experience for our MODERATOR users is currently frustrating. They navigate to the Contact Submissions dashboard, click a highly visible “Delete Entry” button… and are immediately slapped with a “403 Forbidden” error screen because the route rejected them.

UI Gating is the practice of hiding controls and navigation elements that a user does not have permission to utilize.

We must conceptually understand that hiding a button using CSS or EJS if/else blocks is fundamentally not security. It is purely user experience alignment. A malicious user can trigger the underlying POST request utilizing an API tool like Postman regardless of whether the button is rendered on screen. The Express middleware serves as the security. The EJS UI gating serves as the UX.

To conditionally hide interface elements dynamically, the EJS template requires awareness of exactly which capabilities the currently logged-in user possesses. We cleanly achieve this by deploying standard Express res.locals configuration directly inside our primary router endpoint.

routers/adminRouter.js
// Admin: contacts inbox page
router.get("/contacts", async (req, res) => {
const contacts = await _contactOps.getAllContactsAdmin();
// 1. Determine explicitly if the user possesses the pure ADMIN role
const isAdmin = req.user.role === "ADMIN";
// 2. Attach that specific Boolean directly to the response locals
res.locals.canDeleteContacts = isAdmin;
res.render("admin/contacts/index", { contacts });
});

Inside the physical template, we utilize simple condition checks evaluating our explicitly defined target boolean. Moderators will see the submission details and a Read toggle, but only Admins will see the Delete button.

Because we are utilizing client-side JavaScript (fetch()) to handle the deletion action asynchronously rather than a standard HTML form submission, we only need to secure the physical <button> element itself.

views/admin/contacts/index.ejs
<!-- The Action Cluster -->
<div class="admin-actions">
<!-- Visual Status Pill -->
<!-- ... existing Read/Unread radio logic ... -->
<!-- The Delete button renders strictly if the capability resolves to true -->
<% if (canDeleteContacts) { %>
<button class="btn btn-danger contact-delete">Delete</button>
<% } %>
</div>

T.A. Watts Note: Do not repeatedly evaluate complex logic deep within EJS templates (e.g. <% if (user.role === 'ADMIN' || user.role === 'SUPER-ADMIN') { %>). Offload that logic into the server-side router explicitly mapping strict, legible booleans (e.g. canDeleteContacts = true) securely routing it directly into the template.

This gating concept isn’t strictly reserved for the administrative backend either! We can deploy conditional rendering directly against our public index.ejs landing page simply by exposing the current req.user downward.

server.js
// Public Home Route
app.get("/", (req, res) => {
// We pass the currently active user directly to the home page renderer
res.render("index", { user: req.user });
});

With user implicitly injected into the locals object provided to every EJS template, we can cleanly render targeted navigation.

views/index.ejs
<h1>Public Home Page</h1>
<% if (locals.user) { %>
<h2>Welcome back, <%= user.name || user.email %>!</h2>
<p><a href="/admin/logout">Log Out</a></p>
<% } else { %>
<p>You are browsing as a guest.</p>
<p>
<a href="/admin/login">Log In</a> |
<a href="/admin/register">Register</a>
</p>
<% } %>
<hr />
<!-- ... basic site navigation loop ... -->

With our interface intelligently aligned seamlessly with our underlying permissions, we must review the overarching structural pitfalls that typically emerge when deploying RBAC ecosystems.


📘 View-Level Gating Infographic (PNG)


The UI looks great. Let’s review the cardinal rule of security to ensure it stays that way.