Admin
Admin gives your staff one place to manage the app: a guarded area under /admin with a
sidebar, a search palette and a dashboard. It has no features of its own. Users, Events, Forms and other modules add
their pages, sidebar entries and dashboard cards when it is installed. Install it when staff need management screens;
for a settings area of your own, use the App shell directly.
The package is @frigear-nu/admin. It draws the admin area from the components of
UI, the sidebar from Menu and the dashboard from
Widget.
What you get
- Pages (on by default): a dashboard at
/admin, and theadminlayout for every page under/admin. - Admin area (on by default): a sidebar in three groups, a ⌘K palette and the account menu in the sidebar footer.
- Access (on with Authorization): only the
adminrole can open the area. - Account menu (on by default): an "Admin" entry for users who may open the area.
- Helpers for module authors:
extendAdminNav,extendAdminDashboardandadminRoute.
@frigear-nu/module-kit, UI,
Menu, Widget and nuxt-auth-utils.You add:@nuxt/ui.Optional:@frigear-nu/authorization (guards the area by permission) and
@frigear-nu/account (the account menu in the sidebar footer).Quick setup
Install Admin
pnpm add @frigear-nu/admin @nuxt/ui @frigear-nu/authorization
Add it to nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/ui', '@frigear-nu/authorization', '@frigear-nu/admin'],
admin: {
settings: {
permission: 'admin:access', // the default
},
},
authorization: {
roles: {
// `admin` already holds `admin:access`. Give it to a role of your own too:
manager: { label: 'Coordinator', grant: ['admin:access', 'forms:*', 'users:read', 'users:list'] },
},
},
})
Give yourself a role that holds the permission
Only users who hold admin:access can open the area. The module registers it and grants it to the admin role. In the
starter, NUXT_ACL_ADMINS lists the emails that get the admin role. See
Roles and the first admin.
admin:access only opens the pages. The data behind them checks its own permissions, such as forms:manage, so a role
that holds admin:access alone sees an empty area.
admin:access, /admin shows the dashboard with the sidebar,
and the account menu has an "Admin" entry. Signed in without it, /admin shows a 403 error page and the entry is hidden. Signed out,
you are sent to authorization.redirectTo, or get a 401 when it is unset.Options
Options go under the admin key.
| Option | Type | Default | Purpose |
|---|---|---|---|
settings.base | string | '/admin' | The path prefix of the area. A trailing slash is trimmed. |
settings.dashboard | boolean | string | true | The dashboard page at base. false turns it off; a string is a custom path. |
settings.permission | string | false | 'admin:access' | The permission needed to enter the area. false sets no guard. |
settings.layout | boolean | true | Gives every page under base the admin layout. |
settings.backTo | string | false | '/' | Target of the sidebar's "Back to the site" link. false hides it. |
settings.dashboard. With false, other modules' pages under base still get the layout.
settings.permission. It becomes a route rule { permission } on <base>/**, when Authorization is installed.
The default admin:access is registered by Admin and granted to the admin role. A custom permission must be
registered by your app, and given to the roles that may enter. With false, guard the area yourself. A rule of your
own for that path wins.
settings.layout. It sets a route rule { appLayout: 'admin' } on <base> and <base>/**, unless a page picks
its own layout. Your own app/layouts/admin.vue, in the app or a layer, replaces the module's. false registers no
layout and sets no rule.
settings.backTo. A path gets the locale prefix. An absolute URL to another host is used as is.
With @nuxtjs/i18n, every rule is repeated for each /<locale> prefix. The "Admin" entry in the account menu is added
only when dashboard is on and the area is guarded (Authorization is installed and permission is not false). The entry itself carries the permission, so it is hidden from
users who cannot open the area.
Runtime config
runtimeConfig.public.admin holds the resolved values. Set NUXT_PUBLIC_ADMIN_BACK_TO to change backTo per
deploy; an empty value hides the link.
type PublicModuleOptions = {
base: string
dashboard?: string // undefined when the dashboard is off
backTo?: string | false
}
App
Add a page to the sidebar
The sidebar is three groups, each read from its own menu path. Which group a page belongs in is explained on Layouts and navigation.
A module never imports Admin at runtime. It adds its entry to the group's menu path, which works whether or not Admin is installed. For an entry that needs runtime config, register a runtime menu file:
// module.ts
import { addOptionalMenuSource } from '@frigear-nu/module-kit/utils'
addOptionalMenuSource(nuxt, { file: resolver.resolve('./runtime/menus'), menu: 'admin' })
// runtime/menus.ts
import { defineMenuItem, defineMenuSource, extendMenuItem } from '@frigear-nu/menu/utils'
export default defineMenuSource(() => {
const config = useRuntimeConfig().public.forms
// Manage group (the root of `admin.nav`)
extendMenuItem('admin.nav', defineMenuItem({
id: 'forms-admin',
label: 'Forms',
labelKey: 'forms.menu.admin.label',
icon: 'i-lucide-clipboard-list',
to: config.pages.admin,
order: 30,
}))
// Settings group
extendMenuItem('admin.nav.settings', defineMenuItem({
id: 'forms-settings',
label: 'Form settings',
icon: 'i-lucide-settings-2',
to: '/admin/forms/settings',
}))
})
For entries known at build time, call the helpers from @frigear-nu/admin/utils in setup():
import { extendAdminNav, extendAdminDashboard, adminRoute } from '@frigear-nu/admin/utils'
extendAdminNav(nuxt, { label: 'Forms', to: adminRoute(nuxt, 'forms'), icon: 'i-lucide-clipboard-list', order: 20 })
extendAdminNav(nuxt, { label: 'OAuth applications', to: adminRoute(nuxt, 'oauth'), icon: 'i-lucide-key-round' }, 'settings')
extendAdminDashboard(nuxt, { component: resolver.resolve('./runtime/app/widgets/Submissions.vue'), order: 10 })
extendAdminDashboard(nuxt, { component: resolver.resolve('./runtime/app/widgets/SubmissionCount.vue'), order: 10 }, 'top')
| Export | Signature | Purpose |
|---|---|---|
extendAdminNav | (nuxt, items: MenuItem | MenuItem[], group?: 'overview' | 'manage' | 'settings') | Adds sidebar entries. group defaults to 'manage'. |
extendAdminDashboard | (nuxt, widgets: WidgetDefinition | WidgetDefinition[], position?: 'top' | 'main' | 'side') | Adds dashboard widgets. position defaults to 'main'. |
adminRoute | (nuxt, ...segments: string[]) => string | A path below the configured base: adminRoute(nuxt, 'events', ':id') is /admin/events/:id. |
ADMIN_NAV_GROUPS | { overview, manage, settings } | The menu path of each group: admin.nav.overview, admin.nav and admin.nav.settings. |
ADMIN_NAV_MENU | 'admin.nav' | The menu of the Manage group. |
ADMIN_DASHBOARD_AREA, ADMIN_DASHBOARD_TOP_AREA, ADMIN_DASHBOARD_SIDE_AREA | 'admin.dashboard', '.top', '.side' | The three dashboard widget areas. |
Use adminRoute so a module's default paths follow the host's base. A module can also call extendMenu or
extendWidgets directly, without depending on Admin.
Dashboard
The dashboard is three widget areas, so modules fill it without Admin knowing them:
| Widget area | position | Layout | Put here |
|---|---|---|---|
admin.dashboard.top | 'top' | A row of up to four cards | One StatCard per widget. |
admin.dashboard | 'main' | A wide column, full width when the side column is empty | Lists and tables, such as a ListCard. |
admin.dashboard.side | 'side' | A narrow column next to it | Short ListCards. |
Widgets load their own data with useLazyFetch and receive no context. When no module adds a widget, the dashboard
shows a shortcut card for every entry of the Manage and Settings groups.
Layout
The admin layout feeds the admin menus to the shared App shell, which draws the
chrome. The account area uses the same shell.
- Search. The ⌘K palette has one group per sidebar group, so everything in the sidebar is searchable.
- Account menu.
AccountMenufrom Account sits in the sidebar footer, when installed. - Overrides.
AccountMenuandLanguageSelectare resolved by name, so your own versions win and neither is required.
What the sidebar, navbar and width look like is on
Layouts and navigation. The dashboard page (admin-dashboard)
is skipped when your app already has a page at its path.
Components
Admin pages are built from the page components of UI, following the UX patterns. A module that ships admin pages declares Admin as an optional module dependency and UI as a dependency.
The old Admin* components have been removed. Replace them like this:
| Removed | Use instead |
|---|---|
AdminPage | <div class="flex flex-col gap-4"> |
AdminPageHeader | PageHeading: back becomes breadcrumb, the links slot becomes actions |
AdminSection, AdminToolbar | SectionCard, PageToolbar |
AdminEmpty | <UEmpty variant="naked"> |
AdminTable | TableCard around a UTable |
AdminStatCard, AdminListCard | StatCard, ListCard |
AdminKeyValue | DescriptionList; a missing value reads "Not set" instead of a dash |
Translations
Override any string in your app's i18n/locales/<code>.json. The namespace is admin, in English and Danish.
| Key | Used for |
|---|---|
admin.title | The brand label and the start of the navbar line |
admin.nav.label | The sidebar navigation's accessible name |
admin.nav.groups.overview, .manage, .settings | The names of the sidebar groups |
admin.search.goTo | The title of the ⌘K palette groups |
admin.dashboard.title, .description, .empty | The dashboard page |
admin.backToSite | The link back to the public site |
admin.menu.label, .description | The "Admin" entry in the account menu |
Next steps
- Add an admin page – add a page and its sidebar entry, step by step.
- Layouts and navigation – pick the sidebar group for a new page.
- Widget – build a dashboard card for one of the three widget areas.