adhocrat-io/arkhe-main
Bootstrap a Laravel admin backend with Livewire 4 + Flux UI: users, roles & permissions (Spatie), user profiles, SEO + sitemap, and cookie-consent. Interactive installer publishes config/migrations, seeds default roles, and patches sidebar/Tailwind scanning.
Authentification forte du back-office. Un facteur fort — clé d'accès ou 2FA confirmée — peut désormais être exigé avant d'atteindre l'administration. Une clé d'accès dispense de la 2FA : elle est déjà à deux facteurs, et liée au domaine, donc résistante au hameçonnage là où un code TOTP ne l'est pas.
Le verrou porte sur l'accès, pas sur la connexion : la façon dont un
utilisateur s'authentifie appartient au pipeline Fortify de l'app. Un compte
sans facteur reste connecté et garde le reste du site ; seul
/administration/* se ferme jusqu'à son enrôlement. Fortify ne propose rien
d'équivalent — il fournit la 2FA mais ne l'impose jamais, et n'embarque aucun
middleware.
Désactivé par défaut : arkhe.strong_auth.enforce vaut false, donc une
montée de version ne prive personne d'accès. ARKHE_STRONG_AUTH=true protège
tout le back-office. Toute valeur non reconnue se lit comme désactivé : une
faute de frappe ne doit pas enfermer une équipe dehors.
C'est tout ou rien, délibérément. Ne verrouiller que la zone sensible laissait la liste des utilisateurs ouverte — là où l'on crée des comptes et attribue des rôles — ce qui a l'air prudent mais protège peu ; les rôles et permissions tracent déjà cette frontière.
Extensible à vos propres routes. Le verrou est exposé comme alias
réutilisable, arkhe.strong-auth, à poser sur ce que votre app considère comme
faisant partie de l'administration — son tableau de bord au premier chef, qui en
est l'entrée. L'alias est inerte tant que le drapeau est éteint.
Couvert aussi côté Livewire. Le middleware de route ne garde que le premier
affichage : les actions suivantes passent par le point d'entrée de Livewire, qui
ne porte que ['web']. Les trois portails d'Arkhe sont donc déclarés persistants,
et les composants du back-office portent RequiresStrongAuth, qui revérifie
l'exigence côté serveur à chaque requête. Les deux moitiés sont nécessaires : le
middleware persistant s'appuie encore sur le chemin fourni par le client. Une
app qui écrit ses propres composants d'administration doit leur appliquer le
trait.
La détection porte sur les méthodes du modèle, jamais sur les traits ni les
classes : ni laravel/fortify ni laravel/passkeys ne deviennent des
dépendances, et la surface exposée aux montées de version se limite à deux noms
de méthodes publiques.
Un écran d'explication, pas une redirection sèche. Un utilisateur bloqué arrive sur une page Arkhe qui énonce l'exigence, présente les deux options et décrit la suite — y compris la confirmation de mot de passe que la plupart des starter kits placent devant leur page de sécurité. Elle ne renvoie vers celle-ci qu'ensuite.
Cette page existe parce que renvoyer directement ne fonctionnait pas : la page
de sécurité appartient à l'app et se trouve généralement derrière
password.confirm, dont le passage consomme le message en session. L'utilisateur
se retrouvait devant une demande de mot de passe puis un écran de réglages, sans
rien qui lui dise ce qu'on attendait de lui.
Elle vit hors du groupe verrouillé — une porte qui redirige vers une page
qu'elle garde elle-même est une boucle infinie — et reste enregistrée même
verrou éteint, pour qu'un lien périmé trouve une page plutôt qu'un 404. Elle
s'écrase comme n'importe quelle page Arkhe, via
components.strong-auth-required.
Deux états dégradés sont traités plutôt que subis. Sans page d'enrôlement résoluble, Arkhe affiche l'avertissement nommant la clé à configurer, au lieu de rediriger vers rien. Et si le modèle n'expose aucun des deux mécanismes, l'exigence — que personne ne pourrait alors satisfaire — est ignorée avec un avertissement en journal : condamner un back-office que nul ne pourrait plus rouvrir serait pire que le laisser en l'état.
arkhe:main:upgrade-to-v4, la commande de montée de version. Elle retire de
la config publiée les trois clés que le retrait du tableau de bord laisse
mortes — bandeau de commentaire compris, l'édition passant par les tokens PHP
plutôt que par une expression régulière, faute de quoi une mention de la clé
dans un commentaire suffirait à couper le fichier au mauvais endroit.
Elle signale sans réécrire ce qui appartient au consommateur : les vues
publiées appelant une route disparue — arkhe.roles.create lève à l'affichage
de la page, pas au clic — les objets Form restés sur toArray(), et les
sous-classes dont un hook redéfini n'est plus appelé depuis que
l'enregistrement a migré vers EditUser / EditRole.
Elle signale aussi, séparément, les vues publiées qui montrent quelque chose
que le paquet ne veut plus dire. Le cas rencontré sur une app réelle : un
partiel de barre latérale publié avant la fusion rôles/permissions garde une
entrée « Permissions » à côté de « Rôles & permissions ». Rien ne casse — le
lien redirige — mais le doublon s'installe sans que personne le remarque.
Supprimer sa copie reprend la vue du paquet, qui se rend depuis le registre
ArkheNav et reste à jour toute seule.
Les cas silencieux sont précisément ceux qui méritent un rapport : une vue qui lève se voit tout de suite, un menu en double se contemple pendant des mois.
--dry-run n'écrit rien, la commande est idempotente, et elle refuse de tourner
sur une config encore en V2 en renvoyant vers arkhe:main:upgrade-from-v2.
Quatre élévations de privilèges, toutes de la même famille : on pouvait accorder ce qu'on ne détenait pas. Chacune permettait à un compte du back-office de devenir root. Elles sont fermées au niveau des services, donc sur tous les chemins d'écriture — composants, méthodes dépréciées, sous-classes, commandes.
update-role suffisait à s'attribuer
manage-roles en éditant son propre rôle. RoleService n'accorde plus
qu'une permission que l'acteur détient lui-même.permissions de
UserForm n'était pas gardé, alors que les rôles voisins l'étaient :
create-user suffisait à fabriquer un compte porteur de manage-roles,
puis à s'y connecter. UserService applique désormais la même règle.config('arkhe.roles') avait
le rang -1, comme un utilisateur sans rôle : -1 <= -1 ouvrait son
attribution à quiconque, quelles que soient les permissions qu'il portait.
RoleHierarchy distingue maintenant « aucun rôle » de « rôle inconnu » : un
rôle non classé n'est attribuable que par qui détient déjà tout ce qu'il
accorde. canManage() avait le même angle mort.view-user en manage-roles transformait
instantanément tous ses porteurs en administrateurs, sans toucher à un rôle
ni à un compte. PermissionService refuse désormais de renommer ou
supprimer une permission canonique — même pour root, le code du paquet s'y
réfère en dur — et n'autorise à altérer que ce que l'acteur détient.Durcissements associés :
#[Locked] sur les propriétés qui portent l'identité de l'objet édité
(EditRole::$roleId, $isCanonical, EditUser::$userId) : sans lui, le
client les réécrivait avant save() et pivotait vers une autre cible, les
authorize() ne portant que sur la permission, jamais sur quoi.RoleForm relit le caractère canonique depuis la base au lieu de croire son
propre drapeau public : forcé à true, il faisait tomber toutes les règles
du nom, unicité comprise.EditRole::toggleGroup(), publique donc appelable directement, porte sa
propre autorisation.arkhe.admin.roles, chemin de compatibilité V2 — sont
réservés à la création : fabriquer un rôle homonyme et se l'attribuer
ouvrait la porte sans détenir access-backend. Le repli du middleware, lui,
est inchangé : le durcir aurait privé d'accès les apps qui s'en servent,
à la simple montée de version.ListPermissions exige la permission de zone sensible : il reste montable
sur une route applicative et donne accès au socle du RBAC.Vingt tests de non-régression couvrent ces chemins
(tests/Feature/PrivilegeEscalationTest.php).
Le paquet était devenu ininstallable en PHP 8.3. spatie/laravel-sitemap
a relevé son plancher PHP à ^8.4 dans une version mineure : la contrainte
^8.1 ne laissait donc plus que des versions refusant PHP 8.3, alors que le
paquet déclare "php": "^8.3". composer install échouait avant même de
lancer les tests — les quatre jobs PHP 8.3 de la matrice tombaient sur une
erreur de résolution, pas sur un test.
La contrainte passe à ^8.0, qui rouvre la porte à une version acceptant
^8.2 || ^8.3 || ^8.4. Rien ne change pour une app en PHP 8.4 : Composer y
retient toujours la version la plus récente. Vérifié sur un PHP 8.3.33 réel,
suite complète au vert.
Livewire 4.0.x ne fonctionnait pas avec le paquet. La contrainte disait
^4.0, mais un enchaînement de call() sur un composant y renvoie un
instantané nul (HandleComponents::update(), $snapshot['data'] sur null).
Le job Laravel 12 - prefer-lowest de la matrice, seul à retenir la 4.0.0, le
montrait ; toutes les autres combinaisons prennent la 4.2 ou plus et passent.
La contrainte devient ^4.2 : le paquet ne peut pas annoncer une version avec
laquelle il ne marche pas. Aucune app consommatrice n'est concernée, toutes
sont déjà en 4.2 ou au-delà.
Création d'utilisateur impossible depuis l'interface. passwordConfirmation
ne faisait pas partie du contrat sérialisé de UserForm : Livewire ne la
restituait pas au tour suivant, si bien que la confirmation était comparée à
une chaîne vide et qu'une paire pourtant identique était refusée. Le défaut
touchait aussi l'ancien flyout ; aucun test ne le couvrait, les suites
existantes appelant UserService directement.
permission_groups absente du fichier de config publié. La clé était lue
par le code et documentée dans UPGRADE comme le moyen d'imposer son propre
découpage des permissions, mais ne figurait nulle part dans
config/arkhe.php : il fallait lire la doc de montée de version pour
découvrir qu'elle existait. Elle y est désormais, vide — comportement
inchangé, la déduction par convention reste la valeur par défaut — avec sa
documentation. La commande de montée de version l'ajoute aux configs déjà
publiées.
Boutons d'actions sans nom accessible. Le « ⋮ » de chaque ligne, dans la
liste des utilisateurs comme dans celle des permissions, était une icône
seule au libellé vide : ni lecteur d'écran ni infobulle n'avaient de quoi le
nommer. Ils portent maintenant title et aria-label, comme leur homologue
de la liste des rôles qui, lui, les avait déjà.
BREAKING — le tableau de bord quitte le paquet, avec son composant, sa
vue, sa route, son entrée de barre latérale et ses clés de configuration
(dashboard_route, dashboard_route_name, override_fortify_redirect),
ainsi que l'override de fortify.home. Il faisait doublon avec celui que les
starter kits fournissent déjà, en moins riche : trois compteurs
d'utilisateurs contre une page prête à recevoir les indicateurs de l'app.
Pire, une app qui posait ARKHE_DASHBOARD_ROUTE_NAME=dashboard voyait le
sien remplacé par celui du paquet. La page d'accueil du back-office
appartient à l'app ; les compteurs, eux, sont déjà en tête de la liste des
utilisateurs.
Rien à faire pour une app qui n'avait pas défini ARKHE_DASHBOARD_ROUTE — le
cas par défaut, la route n'existait pas chez elle. Celles qui l'avaient posée
trouveront la marche à suivre dans UPGRADE.md ; le point
sensible est celles qui avaient pris le nom dashboard, dont les liens de
logo et de menu pointent vers une route qu'Arkhè ne fournit plus.
Permissions individuelles retirées du formulaire utilisateur. Les droits
passent par les rôles : c'est plus lisible, et un audit n'a plus qu'un endroit
à interroger. Le champ permissions de UserForm n'était affiché par aucune
vue, mais il n'était pas inerte pour autant — il chargeait et
réenregistrait les permissions directes à chaque sauvegarde. Une app qui en
avait accordées par seeder les voyait repasser par syncPermissions(), qui
est destructif, dès qu'on modifiait un numéro de téléphone.
UserService continue de les accepter pour les appels programmatiques —
seeder, commande, code applicatif — avec sa garde anti-escalade intacte. Ce
qui disparaît, c'est le chemin depuis l'interface, y compris pour une requête
Livewire forgée. Le test de non-régression correspondant vise désormais le
service plutôt que le formulaire : une garde éprouvée à travers un écran ne
dit rien des appelants qui ne passent pas par lui.
getRoleNames() sur chaque
ligne, donc chaque utilisateur paginé déclenchait sa propre requête Spatie sur
model_has_roles — neuf requêtes identiques pour neuf utilisateurs affichés.
UserRepository::paginate() charge désormais la relation d'avance. Le coût
SQL était négligeable ; c'est l'hydratation Eloquent, multipliée par le nombre
de lignes, qui se voyait.list-users et list-roles adoptent le
langage visuel des back-offices maison : en-tête titre + description,
compteurs de tête, table encadrée à lignes zébrées avec en-têtes triables
et overlay pendant les requêtes, états vides qui distinguent « rien à
afficher » de « rien qui corresponde aux filtres », et toasts sur chaque
action. Six composants Blade partagés (x-arkhe::page-header, stat-bar,
list-table-wrapper, sortable-header, table-empty-state,
confirm-modal) portent ce langage et sont réutilisables par les paquets
satellites./administration/roles
devient « Rôles & permissions » : chaque ligne donne le libellé du rôle,
son identifiant et le nombre de permissions qu'il porte. Les filtres et le
tri sont persistés dans l'URL sur les deux listes.arkhe.users.create, arkhe.users.edit, arkhe.roles.edit —
servies par les composants EditUser et EditRole, surchargeables via
config('arkhe.components') comme les listes. Le formulaire y est découpé en
sections (identité, photo, sécurité, accès), chaque champ portant sa
description.config('arkhe.roles') et créés par ArkheRolesSeeder :
le code en fait foi, pas un écran. La liste n'offre plus « Créer un rôle » ni
« Supprimer », et sa colonne Actions mène directement à la fiche, qui sert à
régler les permissions. Côté service, rien ne change :
RoleService::create() / delete(), les événements RoleCreated /
RoleDeleted et les permissions create-role / delete-role restent en
place pour les seeders et les commandes.config/arkhe.php et du .env — mais
elle reprend l'en-tête, les sections et les cartes du reste du back-office.
Un badge dit d'emblée si la régénération est automatique ou manuelle, l'état
est séparé des réglages, et les valeurs sont présentées par
x-arkhe::readonly-field : la mise en forme d'un champ sans son cadre, pour
qu'aucun <input disabled> n'invite à cliquer pour rien.robots gagne une infobulle — c'est le seul qui puisse retirer
le site des moteurs de recherche. L'enregistrement se signale par un toast,
comme ailleurs, au lieu d'un bandeau passé par la session.<input type="file"> brut, seul
endroit des fiches qui échappait au langage Flux, laisse place à
x-arkhe::image-upload : aperçu de ce qu'on vient de déposer, glisser-déposer
avec retour visuel, indicateur pendant le téléversement. Le composant est
partagé — toute page ayant une image à recevoir peut s'en servir.users, roles, sitemap…) avec un
« tout cocher » par groupe, plutôt qu'une liste à choix multiple de plusieurs
dizaines de lignes. Le regroupement est déduit de la convention de nommage
<verbe>-<ressource> ; une app peut l'imposer via
config('arkhe.permission_groups'). Un rôle canonique garde son nom et son
guard verrouillés, mais ses permissions restent modifiables.openCreate,
openEdit, save et les hooks beforeSave / afterCreate / afterUpdate
sur ListUsers et ListRoles). Elles ne sont plus appelées par les vues du
paquet — les pages dédiées les remplacent — mais restent en place pour les
sous-classes qui les surchargent. Retrait à la prochaine majeure : portez
vos surcharges sur EditUser / EditRole, qui exposent les mêmes hooks aux
mêmes signatures. Seule exception, afterCreate sur les rôles : il n'a plus
d'équivalent actif, la création ayant quitté l'interface.ListRoles::confirmDelete(), delete(), cancelDelete() et le hook
beforeDelete : la suppression d'un rôle a quitté l'interface. Le paquet ne
les appelle plus ; elles restent pour les sous-classes et partiront à la
prochaine majeure./administration/permissions redirige
désormais vers /administration/roles ; le nom de route
arkhe.permissions.index et le composant Arkhe\Main\Livewire\ListPermissions
restent en place pour ne rien casser, mais seront retirés à la prochaine
majeure. L'entrée « Permissions » disparaît de la barre latérale, l'entrée
« Rôles » couvrant les deux.Form objects no longer lose properties across a Livewire round trip. Livewire
serialises a form into the component snapshot through toArray()
(FormObjectSynth::dehydrate); the four package forms overrode it to mean
"the fields I want to persist", so every property left out of that list came
back at its default value on the very next request.
Two properties were actually affected. UserForm::$passwordConfirmation: a
first submit rejected for an unrelated reason — a duplicate e-mail, say —
re-rendered the form with the confirmation field emptied, and every following
submit then failed with "the password confirmation does not match" although
the operator had touched nothing. And RoleForm::$is_canonical, which
decides which name rules apply, was dropped the same way.
toArray() on UserForm, RoleForm,
PermissionForm and SiteSeoForm is renamed toPayload(), leaving
toArray() with the inherited Livewire behaviour it must keep. Host apps
that call or override any of these — typically from a ListUsers subclass
registered through arkhe.components — must rename accordingly. Nothing else
changes: the payload handed to the services is identical.ArkheRolesSeeder (and therefore arkhe:main:install) now fails fast with
an actionable message when config/arkhe.php still uses the V2
roles/permissions layout, instead of crashing mid-insert with an opaque
Array to string conversion SQL error.arkhe:main:upgrade-from-v2 now rewrites the V2 roles / permissions
config layout into the V3 one: roles becomes a key => name map,
permissions a flat deduplicated list, and the V2 role => permissions
mapping moves verbatim (enum keys and inline comments preserved) into a
new role_permissions entry. The rewrite is tokenizer-based, only touches
the two top-level entries, prompts before writing and honours --dry-run.Form objects no longer lose properties across a Livewire round trip. Livewire
serialises a form into the component snapshot through toArray()
(FormObjectSynth::dehydrate); the four package forms overrode it to mean
"the fields I want to persist", so every property left out of that list came
back at its default value on the very next request.
Two properties were actually affected. UserForm::$passwordConfirmation: a
first submit rejected for an unrelated reason — a duplicate e-mail, say —
re-rendered the form with the confirmation field emptied, and every following
submit then failed with "the password confirmation does not match" although
the operator had touched nothing. And RoleForm::$is_canonical, which
decides which name rules apply, was dropped the same way.
toArray() on UserForm, RoleForm,
PermissionForm and SiteSeoForm is renamed toPayload(), leaving
toArray() with the inherited Livewire behaviour it must keep. Host apps
that call or override any of these — typically from a ListUsers subclass
registered through arkhe.components — must rename accordingly. Nothing else
changes: the payload handed to the services is identical.ArkheRolesSeeder (and therefore arkhe:main:install) now fails fast with
an actionable message when config/arkhe.php still uses the V2
roles/permissions layout, instead of crashing mid-insert with an opaque
Array to string conversion SQL error.ArkheNav guide that a menu can only controls link
visibility — package routes must still be protected by middleware
independently. The menu gate and the route guard are two separate layers.Shared navigation registry (Arkhe\Main\Support\ArkheNav, with
NavSection / NavItem). The backend sidebar is now driven by named
sections so satellite packages can branch onto the same menu — add an entry
to the shared settings ("Réglages") section, or declare their own
collapsible section — without patching any Blade file. Arkhè seeds two default
sections: access ("Accès": users, roles, permissions) and settings
("Réglages": SEO, sitemap, cookies). Example:
use Arkhe\Main\Support\ArkheNav;
ArkheNav::section('settings')->item(
key: 'billing', label: fn () => __('billing::nav.title'),
icon: 'credit-card', route: 'billing.settings', can: 'manage-billing',
);
arkhe::arkhe.settings.title translation ("Réglages" / "Settings") for the
consolidated settings section heading.
arkhe::partials.sidebar-items now renders from the registry instead of a
hardcoded item list. The default menu is unchanged; the [@include](https://github.com/include) host apps
already have keeps working as-is. Roles/permissions/settings visibility is now
gated by the root_permission (matching the route guards) rather than the
root role, so a custom role granted manage-roles sees those items too.whitecube/laravel-cookie-consent (^1.3) integration for GDPR cookie
consent. Arkhe\Main\Cookies\ArkheCookiesServiceProvider pre-registers
Laravel's session + CSRF cookies under "essentials", and the layout
renders [@cookieconsentscripts](https://github.com/cookieconsentscripts) / [@cookieconsentview](https://github.com/cookieconsentview) automatically
when Features::hasCookieConsent() is true (now the default).
Root-only read-only audit page at /administration/cookies (Livewire
alias arkhe.cookies, route arkhe.cookies.index) lists every
registered category/cookie. New permission view-cookies.spatie/laravel-sitemap (^8.1) integration. Arkhe\Main\Services\SitemapService
wraps SitemapGenerator with config-driven URL/path; a scheduled
Arkhe\Main\Jobs\GenerateSitemap job runs daily at the cron expression
set by arkhe.sitemap.schedule (defaults 0 3 * * *); the root-only
Livewire page at /administration/sitemap exposes a "Regenerate now"
button that dispatches the same job onto the host app's queue. Last
generation timestamp persisted on the arkhe_site_seo table via a new
sitemap_generated_at column. Three new permissions
(manage-sitemap, view-sitemap, update-sitemap).ralphjsmit/laravel-seo (^1.8) is now a first-class dependency. The
package's seo() helper is rendered in the Arkhe layout's <head> so
every backend page emits sensible meta tags out of the box. See the new
SEO section in the README.Arkhe\Main\Concerns\HasArkheSeo trait — composes HasSEO behind an
Arkhe-namespaced alias so consumers add per-record SEO with a single
use statement./administration/seo
(Livewire alias arkhe.site-seo, route arkhe.site-seo.edit). Edits
arkhe_site_seo (a singleton table holding site_name, title_suffix,
description, image, author, robots, twitter_username,
favicon). A SEOManager::SEODataTransformer registered in the
service provider merges these defaults into every SEOData rendered
through the layout. Root-only; gated by the new manage-site-seo,
view-site-seo, update-site-seo permissions.arkhe:main:install now offers to publish ralphjsmit/laravel-seo's
migration + config when the seo table is missing.arkhe:main:install now offers to patch the consumer's Tailwind v4
resources/css/app.css with the [@source](https://github.com/source) directive needed to scan the
package's Blade views. Idempotent. Falls back to a printed snippet for
Tailwind v3 setups (tailwind.config.js).Features::hasSeo() and Features::hasCookieConsent() now default to
true. Both became first-class features in 3.1.0; the flags remain as
escape hatches for consumers that want to disable an integration without
removing the dependency.arkhe:main:install now patches the HasBackendProfile trait into the
consumer's App\Models\User before prompting for the root user's
credentials, and no longer requires a second run to finish creating the
user. The model file is resolved via Composer's ClassLoader instead of
Reflection so the class isn't autoloaded ahead of the patch.list-users, list-roles and list-permissions Livewire pages now use
the <flux:table> primitives. Their CSS ships with flux.css (already
imported by every consumer), so they render correctly in dark mode even
if the consumer's Tailwind build doesn't [@source](https://github.com/source) the vendor path.arkhe.install.patch_restart translation key.V3 is a from-scratch rewrite that keeps the V2 public surface (namespace,
provider name, config prefixes) so existing host apps upgrade in place — no
search-replace of use Arkhe\Main\… statements required. The major bump
covers the bumped Spatie dependency (^7), the new permission-based RBAC,
the Livewire 4 page rewrites, and the lifecycle-hook extensibility layer.
arkhe:main:upgrade-from-v2 Artisan command — appends missing V3 keys
(dashboard_route, role_permissions, components, backend_permission,
root_permission, features…) to the host app's config/arkhe.php
without disturbing V2 entries, and rewrites legacy Livewire aliases
(arkhe.main.livewire.admin.users.users-list → arkhe.list-users) inside
resources/views/. Supports --dry-run.ArkheRolesSeeder, mapped to roles via config('arkhe.role_permissions')
(use the literal '*' to grant every permission, typical for root).EnsureUserIsRoot middleware (alias arkhe.root) gating the sensitive
zone (roles + permissions management) on the configurable
arkhe.root_permission (defaults to manage-roles).arkhe.backend middleware now also accepts a role-list fallback through
config('arkhe.admin.roles') — V2-shaped configs without seeded
permissions keep working.config('arkhe.components'). Override
any of list-users, list-roles, list-permissions, dashboard with a
host-app subclass; routes auto-resolve to the configured class.beforeSave, afterCreate, afterUpdate,
beforeDelete) on every Livewire CRUD page — for side-effects that need
UI context (form state, flash messages) rather than a global event.RoleHierarchy support helper — derives the role hierarchy from the
order of config('arkhe.roles') so renaming/inserting a role updates
the seeder AND the assignment rules in one place.ListRoles + ListPermissions Livewire CRUD pages, restricted to the
root area.Dashboard opt-in route registered when ARKHE_DASHBOARD_ROUTE is set,
with optional Fortify home redirect override
(ARKHE_OVERRIDE_FORTIFY_REDIRECT).arkhe:main:add-user CLI command for non-interactive user provisioning.tests/Feature/ExtensibilityTest.php, PermissionSeederTest.php,
ProfileMigrationSkipTest.php, UpgradeFromV2CommandTest.php — extending
coverage to ~78% of src/.spatie/laravel-permission
constraint bumped from ^6.25 to ^7.0. Host apps should run
composer update spatie/laravel-permission --with-all-dependencies during
the V3 upgrade.add_arkhe_profile_columns_to_users_table migration is now idempotent
column-by-column (Schema::getColumnListing + add only the missing ones,
with a symmetric down()). Apps that already had first_name,
last_name, date_of_birth, civility from V2 migrate cleanly.ArkheRolesSeeder is non-destructive — re-running it does not strip
permissions the host app has granted to its own roles.arkhe::layouts.app to layouts::app so
the Livewire starter kit's sidebar wraps Arkhe pages out of the box. Set
ARKHE_ADMIN_LAYOUT=arkhe::layouts.app to fall back to the package's
minimal header-only layout.Arkhe\Main\ preserved — all use Arkhe\Main\… imports in
host apps continue to resolve.Arkhe\Main\ArkheMainServiceProvider preserved — composer's
package discovery picks the V3 provider with no changes to host app
composer.json extra.laravel.providers.admin.prefix, admin.layout,
top-level permissions, roles, role_hierarchy, role_labels) keep
working unchanged. V3 reads the new keys with V2 fallbacks
(arkhe.admin.prefix falls back to arkhe.route_prefix,
arkhe.admin.layout falls back to arkhe.layout).spatie/laravel-package-tools.ArkheMainServiceProvider with config, views, translations, route and migration auto-discovery./administration).EnsureUserHasBackendAccess middleware aliased as arkhe.backend.HasBackendProfile trait (wraps Spatie\Permission\Traits\HasRoles) exposing full_name, avatar_url, initials and the helpers isArkheRoot() / isArkheAdmin().UserRepository + UserRepositoryInterface contract.UserService for mutations (create/update/delete) with avatar upload, role and permission sync, dispatching UserCreated / UserUpdated / UserDeleted events.ListUsers full-page component with Flux UI Free (search, sort, filters, pagination, modal CRUD with avatar upload).arkhe:main:install interactive command (publishes config + migrations, seeds the four default roles, creates a root user).Features::hasCookieConsent() / Features::hasSeo() flags for phase 2.HasBackendProfile.How can I help you explore Laravel packages today?