A protected admin console that your app can drop in to get a working dashboard, account and profile pages, and user management — the final piece of a complete, local auth experience. Combined with ForgeAuth and ForgeAppAuth, it turns the auth primitives into a secure, ready-to-use admin area.
ForgeAdminConsole ships a set of already-wired,
authenticated pages under /admin —
a dashboard, account settings, a profile editor,
and a paginated users list with detail views. It's
built on the auth capability and the app-level
auth flow, so by the time it's installed you have a
real, secure place for a logged-in user to land.
Getting a real, secure admin area takes three cooperating pieces. ForgeAdminConsole is the last of them, and only feels complete because the other two exist:
The primitives: the
UserProvider and
UserContext contracts, roles
and permissions, JWT and API-key support,
and the auth middlewares. It defines
how auth works but doesn't decide
where your users live.
The business layer that makes the engine
usable: it provides the
concrete UserProvider (your
user repository) and
UserContext, plus the real
/auth routes — register,
login, logout, forgot-password, and
reset-password — with forms, validation,
and sessions.
The window into it all: pages gated by the auth route that read the current user and list the user store. It's the easy starting point — a functional admin area with no wiring left to do.
Install all three and the loop closes: a visitor
registers and logs in through ForgeAppAuth's
/auth flow, lands in the protected
console, and you have a secure home for
authenticated management.
php forge.php package:install-module --module=ForgeAdminConsole
The module requires the pieces it builds on —
forge-router, forge-view,
forge-components, and
forge-auth — and links its
stylesheet into your public directory on install.
For the complete flow, also install
ForgeAppAuth alongside it; that's the
module that provides the user store and the
/auth login flow the console depends
on for both authentication and signing out.
The console is a handful of controllers, all under
the /admin prefix:
| Route | Purpose |
|---|---|
/admin |
Dashboard — stats, recent activity, quick actions |
/admin/account |
Account settings (email), GET + POST save |
/admin/profile |
Public profile (identifier, bio), GET + POST save |
/admin/users |
Paginated users table |
/admin/users/{id} |
A single user's detail |
Dashboard data is composed in the controller —
stats cards, a recent-activity feed, and quick
actions — with the authenticated user's
identifier and email in the header. The account and
profile pages sanitize submitted data
and report success or failure through flash
messages before redirecting back to their own
page.
Every console controller is declared with
#[UseMiddleware(['web', 'auth'])].
The auth middleware — provided by the
app-auth flow — checks for a current user:
use Modules\ForgeAuth\Contracts\UserContextInterface;
use Modules\ForgeRouter\Attributes\Layout;
use Modules\ForgeRouter\Attributes\Routable;
use Modules\ForgeRouter\Http\Attributes\UseMiddleware;
use Modules\ForgeRouter\Http\Response;
use Modules\ForgeRouter\Routing\Endpoint;
use Modules\ForgeRouter\Traits\ResponseHelper;
use Modules\ForgeView\Traits\ViewHelper;
#[Routable(prefix: '/admin')]
#[UseMiddleware(['web', 'auth'])]
#[Layout("ForgeComponents:wrappers/admin-default")]
final class Account
{
use ResponseHelper;
use ViewHelper;
public function __construct(
private readonly UserContextInterface $userContext,
) {}
#[Endpoint("/account")]
public function editAccount(): Response
{
$user = $this->userContext->current();
return $this->view(view: "admin/account", data: [
'currentUser' => $user,
]);
}
}
A guest hitting any admin URL is saved an intended
URL and redirected to
/auth/login — after signing in they
return to where they were headed. Signing out
works through the same flow: the console's user
dropdown posts a logout action to
/auth/logout, closing the loop.
The current user is read through
UserContextInterface, so whatever your
app-auth implementation exposes as the
authenticated user flows straight into the
console header.
The users list and detail pages read through a
small UserProvider helper that wraps
the auth capability's
UserProviderInterface — the same seam
your app-auth module supplies. It paginates the
user store and reshapes rows into table columns:
use Modules\ForgeAuth\Contracts\UserProviderInterface;
final class UserProvider
{
public function __construct(
private readonly UserProviderInterface $userProvider,
) {}
public function getUsersTableData(int $page = 1, int $perPage = 10): array
{
$users = $this->userProvider->paginate($page, $perPage);
return [
'columns' => [
['key' => 'id', 'label' => 'ID'],
['key' => 'identifier', 'label' => 'Identifier'],
['key' => 'email', 'label' => 'Email'],
],
'rows' => array_map([$this, 'userToRow'], $users->items()),
];
}
public function getUserDetails(int $id): ?array
{
$user = $this->userProvider->findById($id);
return $user === null ? null : $this->userToArray($user);
}
}
A missing user redirects back to the list with a
"User not found" flash message rather than erroring.
Because everything flows through the
UserProviderInterface contract, the
console works against whichever user store your
app provides — no changes to the console itself.
All console pages render inside the same admin
layout — ForgeComponents:wrappers/admin-default —
and drive it through the component system. Each
view sets its sidebar, breadcrumbs, and the user
dropdown in the standard way:
use Modules\ForgeComponents\Definitions\Admin\SidebarDefinition;
use Modules\ForgeComponents\Definitions\Admin\NavGroupDefinition;
use Modules\ForgeComponents\Definitions\Admin\NavItemDefinition;
use Modules\ForgeComponents\Definitions\Admin\IconDefinition;
use Modules\ForgeComponents\Definitions\Admin\UserDropdownDefinition;
use Modules\ForgeComponents\Definitions\Admin\DropdownItemDefinition;
$layoutProps = array_merge($layoutProps ?? [], [
'sidebar' => new SidebarDefinition(
brand: 'Admin',
brandHref: '/admin',
groups: [
new NavGroupDefinition(items: [
new NavItemDefinition(label: 'Dashboard', href: '/admin', icon: new IconDefinition(name: 'home'), active: is_link_active('/admin')),
new NavItemDefinition(label: 'Users', href: '/admin/users', icon: new IconDefinition(name: 'users'), active: is_link_active('/admin/users')),
]),
],
),
'userDropdown' => new UserDropdownDefinition(
name: $currentUser?->getIdentifier() ?? 'User',
email: $currentUser?->getEmail() ?? '',
items: [
new DropdownItemDefinition(label: 'Profile', icon: new IconDefinition(name: 'user'), href: '/admin/profile'),
new DropdownItemDefinition(divider: true),
new DropdownItemDefinition(label: 'Logout', icon: new IconDefinition(name: 'arrow-right-on-rectangle'), href: '/auth/logout', method: 'POST'),
],
),
]);
The active nav item is derived from the current URL
via is_link_active(), so highlighting
stays correct as you move between pages. Dashboard,
account, and users pages reuse the shared admin
components — stats cards, data cards, an activity
list, quick-actions tiles, and a table — so the
console looks consistent without bespoke markup.
The console's own stylesheet intersects with the component design system (CSS variables, spacing scales), and the brand shown in the sidebar is configurable.
The shipped pages are a starting point, not a straitjacket. You can:
/admin — same
UseMiddleware(['web', 'auth'])
declaration, same layout.
RequiresPermission / roles), which
layer on top of the base authentication.
UserContextInterface::current()
anywhere in your own controllers for
authenticated, user-scoped behavior.
Because the console is plain controllers using the component system, extending it follows the same conventions as building any other page in your app.
Two small settings live under
forge_admin_console:
| Config | Default | Purpose |
|---|---|---|
| brand | Admin | Sidebar brand label/href |
| items_per_page | 10 | Users per table page |
There's nothing else to set up — the real work is done by the auth capability and the app-auth flow it builds on, and the console simply sits on top of them.