Route Guards
A guard is a <guard> around the content it protects.
Put one in a layout.html and it covers every page in that folder,
so the check is written once:
<!-- pages/admin/layout.html -->
<Layout>
<guard access="admins" redirect="/login">
<AdminSidebar />
<slot />
</guard>
</Layout>
Now nothing under /admin renders for a visitor without
admins access.
What a denied visitor sees
A denied guard always navigates. It never renders a 403 page in place of the children, and the children are never mounted.
Where it navigates to is resolved in three steps, first hit wins:
redirect="/login"on the tag. Explicit beats everything.-
guards.redirectinconfig/jaf.config.js- the project-wide default. /, the site index. There is always somewhere to go.
// config/jaf.config.js
export default {
guards: { redirect: '/login' },
}
So a project with a login page names it once and writes
<guard access={...}> everywhere else. The navigation replaces the current
history entry, so Back does not bounce the visitor into the guarded page again.
Sending them back afterwards
Every denial appends the location the visitor was trying to reach as a
return query parameter, so /account?tab=billing denied to
/login becomes:
/login?return=%2Faccount%3Ftab%3Dbilling
It is a plain path with the base path already stripped, ready to hand to
navigate(). Read it with a url() state variable:
<!-- pages/login.html -->
<script>
import { navigate, guards } from 'jafjs/router'
let returnTo = url('return')
async function signIn() {
await api.login()
guards.access = true
navigate(returnTo || '/')
}
</script>
<button onclick="signIn()">Sign in</button>
A destination you write with a return of its own is left exactly as written -
the author said where to come back to, and the guard is not better informed. The fragment is
dropped: it addresses a position on the page, not the page.
Treat return as user input. It comes from the URL bar and
anyone can edit it.
It never loops. A destination that is the page the visitor is already on would be denied again on arrival, so it is not navigated to at all: the guard renders nothing, and in dev it warns naming the guard and the destination. That is the shape a login page guarded by its own guard has.
Changed in 0.1. A guard with no redirect used to render
pages/errors/403.html in place of its children. Denial is one thing now, and
it is a navigation. If your app kept denial content in a 403 page, move it to the page
you redirect to - see Error Pages.
redirect= takes a literal path. redirect={somePath} is a build
error, and so is a value mixing text and an expression
(access="team-{id}"): a destination is decided before anything renders, so
it has to be knowable when the component compiles.
Who is allowed
access="admins" asks whether admins is in a global
allow list. Seed that list from a config file:
// config/guards.config.ts
export default {
access: true, // Global access flag
allow: ['admins', 'premium'], // Allow list
file: '/api/access.json', // Optional: load from backend
}
And change it at runtime when someone signs in or out:
<script>
import { guards } from 'jafjs/router'
function onSignIn(user) {
guards.allow(user.permissions) // ['admins', 'editors']
}
function onSignOut() {
guards.clear()
}
</script>
| Method | What it does |
|---|---|
guards.allow(list) |
Replace the allow list |
guards.add(name) / guards.remove(name) |
Add or drop one permission |
guards.has(name) |
Check one, in your own code |
guards.clear() |
Empty the list |
guards.access |
The flag a <guard> with no access uses |
guards.add('admins') does not re-open a guard that is already
on screen. A guard whose answer changes while the page is open should read
your own state instead: <guard access={isAdmin}>.
Related
-
Guard component - every form the
accessprop takes, and how it re-evaluates - Layouts - what a layout wraps