Visual Editor - v1.0.0
Site Editor Access Gate
The site-editor SPA at /visual-editor/site/{path?} is the package's most
sensitive surface — it exposes templates, patterns, global styles, menus
and template parts for the whole site. The package does not assume it
knows who should reach it. Each consuming application decides.
This document describes the contract a consumer implements to control that access, and the gates the package ships out of the box.
The contract
namespace ArtisanPackUI\VisualEditor\SiteEditor\Gates;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
interface SiteEditorAccessGate
{
public function check( Request $request ): ?Response;
}
- Return
nullto allow the request — the route renders the SPA mount view. - Return a
Responseto short-circuit the request — the route returns that response verbatim. This is the hook for install-gate pages, login redirects, 403 / 503 templates, or anything else the consumer wants to show in place of the editor.
The package resolves whatever is bound to SiteEditorAccessGate::class
from the container on each request to the site-editor route. To change
the behaviour, bind your own implementation in a service provider.
Package default — fail closed
If a consuming app does not bind a gate, the package binds
DenyByDefaultGate automatically. That gate returns a 503 view on every
request explaining the editor has not been configured. A fresh
install cannot expose the site editor by accident.
Bundled CmsFrameworkInstallGate
For the historical behaviour — allow when cms-framework's SiteEditor module is booted, render the install-instructions page otherwise — bind the bundled gate directly:
// AppServiceProvider::register()
use ArtisanPackUI\VisualEditor\SiteEditor\Gates\CmsFrameworkInstallGate;
use ArtisanPackUI\VisualEditor\SiteEditor\Gates\SiteEditorAccessGate;
$this->app->bind( SiteEditorAccessGate::class, CmsFrameworkInstallGate::class );
This is the right choice for dev / demo apps where any authenticated visitor should reach the editor as long as cms-framework is installed.
Composing your own gate
Production CMS hosts almost always want at least two checks: an
authorisation check (is this user an admin?) and an install check (is
cms-framework actually on the classpath?). The recommended pattern is
to wrap CmsFrameworkInstallGate and run it before your auth check, so
an unauthorised visitor still sees the install instructions in a
half-installed state rather than a 403 that leaks deployment state.
namespace App\SiteEditor;
use ArtisanPackUI\VisualEditor\SiteEditor\Gates\CmsFrameworkInstallGate;
use ArtisanPackUI\VisualEditor\SiteEditor\Gates\SiteEditorAccessGate;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class MyAppSiteEditorGate implements SiteEditorAccessGate
{
public function __construct(
protected CmsFrameworkInstallGate $installGate,
) {}
public function check( Request $request ): ?Response
{
if ( $denial = $this->installGate->check( $request ) ) {
return $denial;
}
$user = $request->user();
if ( ! $user ) {
return redirect()->route( 'login' );
}
if ( ! $user->hasRole( 'admin' ) ) {
return response()->view( 'errors.403', status: Response::HTTP_FORBIDDEN );
}
return null;
}
}
Bind it the same way:
// AppServiceProvider::register()
$this->app->bind( SiteEditorAccessGate::class, MyAppSiteEditorGate::class );
Contract guarantees
- The gate is resolved per-request, so it can depend on request-scoped state (the authenticated user, the route, the session).
- The gate runs inside the
webmiddleware group, so session, CSRF and auth state are available on theRequest. - The package binds its default with
bindIf, so a consumer-supplied binding registered earlier in the boot order always wins. - Implementations must not throw on the unauthenticated /
unauthorised path — return a
Responseinstead so the user sees a useful page rather than a generic framework error.
Testing a custom gate
Bind a stub in the test's beforeEach and assert the route's
behaviour:
beforeEach( function (): void {
$this->app->bind( SiteEditorAccessGate::class, function () {
return new class implements SiteEditorAccessGate
{
public function check( Request $request ): ?Response
{
return null; // allow
}
};
} );
} );
See tests/Feature/SiteEditor/SiteEditorAccessGateTest.php for the
full contract test set.