PHPackages                             nubitio/admin-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. nubitio/admin-bundle

ActiveSymfony-bundle[Authentication &amp; Authorization](/categories/authentication)

nubitio/admin-bundle
====================

One-line Symfony bundle for the Nubit admin stack: API Platform grid contract, translated OpenAPI docs with x-crud hints, dual cookie/Bearer JWT auth, and single-tenant defaults. Pairs with @nubitio/react-admin.

v0.12.1(2w ago)0272↑60%MITPHPPHP &gt;=8.3

Since Jun 11Pushed 2w agoCompare

[ Source](https://github.com/nubitio/admin-bundle)[ Packagist](https://packagist.org/packages/nubitio/admin-bundle)[ Docs](https://github.com/nubitio/nubit-symfony/tree/main/packages/admin-bundle)[ RSS](/packages/nubitio-admin-bundle/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (44)Versions (35)Used By (0)

nubitio/admin-bundle
====================

[](#nubitioadmin-bundle)

One-line backend for the Nubit admin stack. Install it, point [`@nubitio/react-admin`](https://www.npmjs.com/package/@nubitio/react-admin) at your API, and you have a CRUD admin system.

```
composer require nubitio/admin-bundle
```

Registers automatically:

- The **API Platform bridge** from `nubitio/api-platform`: `DataGridFilter`, translated OpenAPI docs with `x-crud` hints, pagination headers, domain-exception mapping.
- **Dual JWT auth**: `POST /api/auth/login`, `/api/auth/refresh`, `/api/auth/logout`, `/api/auth/change-password`, `GET /api/me`. Web clients get HttpOnly cookies; mobile/API clients get tokens in the body (`response_mode: json` or `X-Client-Type: android|ios`). Refresh tokens are rotated and stored hashed (Doctrine entity `nubit_refresh_token`); changing the password revokes every session and re-issues tokens for the current one. Purge old tokens with `bin/console nubit:auth:purge-refresh-tokens`.
- **Mercure** (`nubit_admin.mercure.enabled: true`): issues the `mercureAuthorization` subscriber-JWT cookie on login/refresh so the React grids receive live updates. Replace `MercureCookieDecorator` to scope topics per tenant/user.
- **Fail-safe Mercure publishing** (`mercure.fail_safe`, on by default whenever MercureBundle is installed): API Platform publishes `mercure: true` updates after the flush, so a dead hub used to turn an already-persisted write into a 500 — clients retry and duplicate data. The bundle decorates the default hub: during HTTP requests publish failures are logged and swallowed (response stays 2xx, live refresh degrades to manual); in messenger workers and console commands they are rethrown, so routing `Symfony\Component\Mercure\Update` to an async transport keeps full retry/delivery semantics. Apps with a custom hub name decorate it themselves with `Nubit\AdminBundle\Mercure\FailSafeHub`.
- **Soft delete**: mark entities with `#[Nubit\ApiPlatform\Attribute\SoftDeletable]` and the registered Doctrine filter (`nubit_soft_delete`) hides rows whose `deleted_at` is set. Opt-in per entity by design.
- **Single-tenant defaults** for the `Nubit\Platform` contracts (registry, connection switcher, feature checker, quota enforcer) — multi-tenant apps override the aliases.
- **Autoconfiguration** for `GridVirtualFieldInterface` and `LoginResponseDecoratorInterface` implementations.
- **Discovery CLI**: `bin/console nubit:discover` lists API Platform resources, embedded-lines routes, and (when installed) sequence/workflow features.
- **Embedded lines in docs**: `x-embedded-lines` on parent resources lets `SchemaCrudPage` infer `formDetail` line fields automatically. Set an explicit `route` on `#[EmbeddedLines]` (omitting it is deprecated).

Setup
-----

[](#setup)

1. Import the routes (`config/routes/nubit_admin.yaml`):

```
nubit_admin:
    resource: '@NubitAdminBundle/config/routes.php'
```

2. Wire the firewall (`config/packages/security.yaml`) — the bundle cannot define firewalls for you. **Apps with more than one user provider** (e.g. an extra admin firewall) must also alias the one the API uses, otherwise autowiring is ambiguous:

```
# config/services.yaml
Symfony\Component\Security\Core\User\UserProviderInterface: '@App\Security\ApiUserProvider'
```

```
security:
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
    providers:
        app_users:
            entity: { class: App\Entity\User, property: email }
    firewalls:
        api:
            pattern: ^/api
            stateless: true
            provider: app_users
            custom_authenticator: Nubit\AdminBundle\Auth\JWTAuthenticator
    access_control:
        - { path: ^/api/auth/(login|refresh), roles: PUBLIC_ACCESS }
        - { path: ^/api/docs, roles: PUBLIC_ACCESS }
        - { path: ^/api, roles: ROLE_USER }
```

3. Create the refresh-token table: `bin/console make:migration && bin/console doctrine:migrations:migrate` (the bundle's `RefreshToken` entity is auto-mapped).

Session profile (`GET /api/me`)
-------------------------------

[](#session-profile-get-apime)

The React `SessionProvider` calls this on boot. Default response:

```
{ "username": "admin@example.com", "roles": ["ROLE_ADMIN"], "appProfile": "internal" }
```

`app_profile`Extra blocks`internal`none (single-org panel)`saas``tenant` (when `TenantContext` is set), `features` (from `FeatureCheckerInterface::getEntitlements()`)`hybrid`same as `saas` — branch/context fields come from a custom `MeResponseBuilderInterface`Alias `MeResponseBuilderInterface` to add application-specific fields without forking the route.

Embedded lines (master-detail forms)
------------------------------------

[](#embedded-lines-master-detail-forms)

Line entities that belong to a parent document use `#[EmbeddedLines]` on the Doctrine class — the bundle registers `GET /api/{lines}` returning a **plain JSON array** for SmartCrud `formDetail` reload (no Hydra envelope, no custom controller).

```
#[EmbeddedLines(
    parentProperty: 'document',
    normalizationGroups: ['document:read'],
)]
#[ORM\Entity]
class SalesDocumentLine { ... }
```

Import embedded line routes in addition to the bundle routes:

```
nubit_embedded_lines:
    resource: '@NubitAdminBundle/config/embedded_lines_routes.yaml'
```

On the parent processor, extend `AbstractEmbeddedLinesProcessor` to bind lines before persist. Frontend:

```
formDetail: {
  propertyName: 'lines',
  url: embeddedLinesUrl('/api/sales_document_lines', 'document'),
  fields: [...],
}
```

Runtime config (`GET /api/runtime-config`, opt-in)
--------------------------------------------------

[](#runtime-config-get-apiruntime-config-opt-in)

Separate from `/api/me`: UI flags, defaults, capabilities, onboarding state — **free-form JSON**defined by the application. Enable the route, implement the provider, alias it:

```
# config/packages/nubit_admin.yaml
nubit_admin:
    runtime_config: true
```

```
// src/Runtime/AppRuntimeConfigProvider.php
final readonly class AppRuntimeConfigProvider implements RuntimeConfigProviderInterface
{
    public function getConfig(): array
    {
        return [
            'ui' => ['showBranchPicker' => false],
            'defaults' => ['currency' => 'USD'],
        ];
    }
}
```

```
# config/services.yaml
Nubit\AdminBundle\Runtime\RuntimeConfigProviderInterface: '@App\Runtime\AppRuntimeConfigProvider'
```

On the React side, `useRuntimeConfig()` from `@nubitio/react-admin` fetches the payload (`RuntimeConfig` is `Record` — type it per app). Disabled by default so internal skeletons work with zero config.

Configuration (defaults shown)
------------------------------

[](#configuration-defaults-shown)

```
# config/packages/nubit_admin.yaml
nubit_admin:
    app_profile: internal   # internal | saas | hybrid
    auth:
        secret: '%env(APP_SECRET)%'   # >= 32 bytes (HS256)
        access_token_ttl: 3600
        refresh_token_ttl: 1209600    # 14 days
        cookie_secure: true
    api:
        translated_docs: true
        docs_locale: '%env(default::APP_API_LOCALE)%'
    mercure:
        enabled: false                # true → mercureAuthorization cookie on login/refresh
        secret: '%env(MERCURE_JWT_SECRET)%'
        topics: ['*']
        hub_path: /.well-known/mercure
        fail_safe: true               # dead hub never turns a successful write into a 500
    audit:
        enabled: false                # true → audit trail (see below)
        ignored_fields: [createdAt, updatedAt, password]
        purge_retention_days: 365
    media:
        enabled: false                # true → media library (see below)
        storage:
            filesystem: null          # FilesystemOperator service id (e.g. S3); null → local
            local_directory: '%kernel.project_dir%/var/uploads'
        directory: media              # sub-directory inside the storage
        purge_retention_days: 30
    runtime_config: false             # true → GET /api/runtime-config
    soft_delete: true                 # nubit_soft_delete Doctrine filter
    single_tenant_defaults: true
```

Audit trail (opt-in)
--------------------

[](#audit-trail-opt-in)

`audit.enabled: true` records field-level before/after diffs for entities marked `#[Nubit\ApiPlatform\Attribute\Auditable]` (creates, updates, deletes — captured from the Doctrine change set, written to `nubit_audit_log` in the same request, attributed to the authenticated user). Serve them to the `AuditTrailPanel` in `@nubitio/react-admin`:

```
#[Auditable]                       // or #[Auditable(resource: 'products')]
#[ORM\Entity]
class Product { ... }
```

```
defineResource('/api/products', {
  auditTrail: { enabled: true, apiUrl: (id) => `/api/audit-trail/product/${id}` },
})
```

`GET /api/audit-trail/{resource}/{id}` returns newest-first entries in the panel shape: `[{ id, timestamp, user, action, changes: { field: { before, after } } }]`. Relations collapse to their id; `ignored_fields` are excluded from diffs; collection contents are not audited. Create the table with a migration and schedule `bin/console nubit:audit:purge`.

Media library (opt-in)
----------------------

[](#media-library-opt-in)

`media.enabled: true` exposes a ready-made upload pipeline matching `fileField()` / `imageField()` in `@nubitio/react-admin` (instant upload — the form submits only the media IRI):

- `POST /api/media` — traditional `multipart/form-data` upload (field `file`), returns `{ id, path, originalName, mimeType, size }` where `path` is the resolved public URL.
- `GET /api/media/{id}` / `DELETE /api/media/{id}` — delete is a **soft**delete; files are removed later by `bin/console nubit:media:purge`(schedule it — instant uploads orphan files when forms are abandoned).
- `GET /api/media/{id}/file` — default streaming endpoint, works for any Flysystem storage behind the same `/api` firewall.

Storage is **local disk by default** (zero config). For S3 (or anything Flysystem speaks), point `media.storage.filesystem` at a `FilesystemOperator`service — e.g. with [oneup/flysystem-bundle](https://github.com/1up-lab/OneupFlysystemBundle):

```
nubit_admin:
    media:
        enabled: true
        storage:
            filesystem: 'oneup_flysystem.default_filesystem_filesystem'
```

To serve direct S3/CDN URLs instead of streaming through PHP, implement `Nubit\AdminBundle\Media\MediaUrlResolverInterface` and alias it in `services.yaml`. Create the table with a migration (`doctrine:migrations:diff`picks up `nubit_media` once enabled). Reference uploads from your entities as a plain `ManyToOne` to `Nubit\AdminBundle\Media\Entity\Media`.

Clients
-------

[](#clients)

**Web (`@nubitio/core`)** — works out of the box: login stores HttpOnly cookies; `CoreProvider` auto-refreshes via `auth/refresh`.

**Android / API** — send `response_mode: "json"` on login (or the `X-Client-Type: android` header on every auth call):

```
POST /api/auth/login
{ "username": "user@example.com", "password": "...", "response_mode": "json" }
→ { "user": {...}, "token": "...", "refreshToken": "...", "expiresAt": 1789... }
```

Refresh with `{ "refreshToken": "..." }` in the body; send `Authorization: Bearer ` on every request.

Extension points
----------------

[](#extension-points)

HookPurpose`MeResponseBuilderInterface`Shape `GET /api/me` (session profile for `@nubitio/react-admin`) — alias your implementation to add branch, currency, or domain context`RuntimeConfigProviderInterface`Shape `GET /api/runtime-config` (UI flags, defaults, capabilities) — alias your implementation; enable with `runtime_config: true``TokenClaimsProviderInterface`Add claims (user id, role, branch, tenant) to JWTs and shape the login response `user` payload — alias your implementation over the default`LoginResponseDecoratorInterface`Attach extra cookies to the web login/refresh response (e.g. a Mercure subscriber JWT) — autoconfigured by interface`RefreshTokenStoreInterface`Swap the Doctrine store for Redis/other`MediaUrlResolverInterface`Emit direct S3/CDN URLs for media instead of the streaming route`GridVirtualFieldInterface`Grid fields without ORM mapping — autoconfigured by interface`Nubit\Platform` tenant/feature/quota aliasesOverride for multi-tenant SaaSLicense
-------

[](#license)

MIT

###  Health Score

45

—

FairBetter than 91% of packages

Maintenance97

Actively maintained with recent releases

Popularity16

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

34

Last Release

15d ago

PHP version history (3 changes)v0.2.0PHP &gt;=8.5

v0.10.5PHP &gt;=8.2

v0.10.8PHP &gt;=8.3

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/990211?v=4)[Johan Guerreros](/maintainers/johangm90)[@johangm90](https://github.com/johangm90)

---

Top Contributors

[![johangm90](https://avatars.githubusercontent.com/u/990211?v=4)](https://github.com/johangm90 "johangm90 (26 commits)")

---

Tags

jwtsymfonybundlecrudadminapi-platformnubit

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/nubitio-admin-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/nubitio-admin-bundle/health.svg)](https://phpackages.com/packages/nubitio-admin-bundle)
```

###  Alternatives

[open-dxp/opendxp

Content &amp; Product Management Framework (CMS/PIM)

9421.6k64](/packages/open-dxp-opendxp)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.4M215](/packages/sulu-sulu)[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k5.9M754](/packages/sylius-sylius)[contao/core-bundle

Contao Open Source CMS

1231.6M2.8k](/packages/contao-core-bundle)[pimcore/pimcore

Content &amp; Product Management Framework (CMS/PIM/E-Commerce)

3.8k3.8M512](/packages/pimcore-pimcore)[chameleon-system/chameleon-base

The Chameleon System core.

1028.7k5](/packages/chameleon-system-chameleon-base)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
