ForgeAdminConsole

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.

Overview

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.

What arrives out of the box

  • A dashboard with stats, activity, and quick actions
  • Account and profile edit pages
  • A paginated users table and user detail
  • Every page behind the auth gate

The Auth Puzzle

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:

1. ForgeAuth — the engine

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.

2. ForgeAppAuth — the local auth flow

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.

3. ForgeAdminConsole — the protected UI

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.

Installation

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

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.

Protected Routes

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.

User Management

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.

The Layout & UI

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.

Extending

The shipped pages are a starting point, not a straitjacket. You can:

  • Add more protected endpoints under /admin — same UseMiddleware(['web', 'auth']) declaration, same layout.
  • Gate finer-grained actions with the auth capability's roles and permissions (RequiresPermission / roles), which layer on top of the base authentication.
  • Read UserContextInterface::current() anywhere in your own controllers for authenticated, user-scoped behavior.
  • Customize the sidebar, breadcrumbs, and user dropdown the same way the shipped views do.

Because the console is plain controllers using the component system, extending it follows the same conventions as building any other page in your app.

Configuration

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.