Account
The account area as a Nuxt module: an account layout with a left
navigation, and opt-in pages built with @nuxt/ui and the
@frigear-nu/ui page patterns over the self-service routes of Users and the token
routes of Auth. It owns no tables and registers no server routes of its own. The screens follow the
UX patterns.
@nuxthub/core, @frigear-nu/module-kit, @frigear-nu/auth,
@frigear-nu/users and @frigear-nu/ui (which brings @nuxt/ui). Optional:@frigear-nu/menu (the account navigation, the dropdown and the overview page) and
@frigear-nu/widget (the account.overview widget area).Quick setup
npx nuxt module add @frigear-nu/account
export default defineNuxtConfig({
modules: [
'@nuxthub/core',
'@frigear-nu/users',
'@frigear-nu/menu', // optional
'@frigear-nu/account',
],
account: {
settings: {
pages: {
profile: true, // or a custom path, e.g. '/me'
security: true,
index: true, // the overview, only registered with @frigear-nu/menu
documents: true,
apiTokens: true,
},
documents: [
{ id: 'handbook', label: { da: 'Håndbog', en: 'Handbook' }, path: '/assets/files/handbook.pdf' },
{ id: 'crew', label: 'Crew plan', path: '/assets/files/crew.pdf', roles: ['admin', 'crew'] },
],
},
},
})
Options
All options live under account.settings.
| Option | Type | Default | Purpose |
|---|---|---|---|
layout | boolean | true | Renders the account area in the account layout (see Layout). false leaves the layout to the app. |
pages.index | string | boolean | off | The overview (summary, widgets, shortcuts). Needs @frigear-nu/menu; skipped with a warning without it. true = /account. |
pages.profile | string | boolean | off | Profile page (name, avatar, email). true = /account/profile. |
pages.security | string | boolean | off | Password and passkeys. true = /account/security. |
pages.documents | string | boolean | off | Lists documents for download. true = /account/documents. |
pages.apiTokens | string | boolean | off | Create and revoke personal API tokens. true = /account/api-tokens. |
documents | AccountDocument[] | [] | Files offered on the documents page. |
A page is enabled with true (default path), a custom path string, or left off. A page whose path already exists in
the app is not registered, so an app replaces a shipped page by declaring its own at that path.
type AccountDocument = {
id: string
/** A string, or a locale map (falls back to `en`, then the first value). */
label: string | Record<string, string>
/** Where the file is served from, e.g. `/assets/files/handbook.pdf`. */
path: string
icon?: string
/** Only listed for users whose session `role` is one of these. Omit to list for every signed-in user. */
roles?: string[]
}
roles only filters the list. A file served from public/ can be fetched by anyone who knows its URL; put sensitive
files behind a server route if that matters.API tokens and email changes
The API tokens page lists the user's tokens in a table; Create API token opens a slideover with the name, scopes and
expiry, and the new token is shown once above the table with a copy button. Revoking a token asks for confirmation.
Tokens are created through Auth. When users.settings.mcp is on, the page also shows a "Connect to
MCP" card (MCP).
Verified email changes are a setting of Users (users.settings.verifyEmailChange); point users.settings.profilePath
at the profile page, which shows the result. See Users.
Layout
Every page under /account (and /<locale>/account), plus any configured page path outside it, renders in the
account layout. The module sets this with an appLayout route rule, so pages other modules add under /account
(membership, tickets, applications) get it too, whenever they register. A page that sets layout in its own
definePageMeta keeps its choice.
The layout renders the app's default layout (the site header and footer) and puts the account area inside it:
- on wide screens a left column (
UPageAside) with the signed-in user and a vertical navigation built from theaccountmenu (useMenu('account'), throughuseNavLinks); the overview is only active on its own path; - on narrow screens the same entries as a
PageTabsstrip above the content; AccountHeader, then the page.
Without @frigear-nu/menu the navigation lists the enabled pages from settings.pages instead. An app without a
default layout still works: the account area then renders on its own. The app's app.vue must render
<NuxtLayout> for any layout to apply.
settings.layout: false turns this off: no layout is registered and the pages render in whatever layout the app picks.
The shipped pages bring no container or heading of their own, so wrap them in a layout that renders <AccountHeader />.
Heading
AccountHeader is a PageHeading. Its title is the page's title meta, else the label
of the account menu item the route belongs to (the longest matching path, so /account/membership/success still reads
"Membership"), else "Account"; the description comes from the same menu item. It also sets the document title.
AccountHeaderRight fills its actions on every page. A page hides the heading with:
definePageMeta({ heading: false })
A page another module adds needs nothing but its route and an account menu item to get the navigation and heading.
It should not draw its own container or account header; its root is a plain <div class="flex flex-col gap-4">.
Overview
The overview (pages.index) shows a summary card (avatar, name, email and AccountHeaderRight), then the
account.overview widget area when @frigear-nu/widget is installed, then a card with shortcuts to
every other entry of the account menu. Widgets receive the session user as a prop:
// In another module's setup()
import { extendWidgets } from '@frigear-nu/widget/utils'
extendWidgets(nuxt, 'account.overview', {
id: 'next-ticket',
component: resolver.resolve('./runtime/app/components/NextTicket.vue'),
order: 10,
})
Use StatCard for numbers and ListCard for short lists, as on the admin dashboard.
Server
The account routes call the self-service routes of Users (/api/users/me/*),
which require a cookie session.
App
const { updateProfileDetails, changeEmailAddress, changePassword, pending, error } = useAccount()
await updateProfileDetails({ name: 'Ada Lovelace', avatar: 'https://example.com/ada.png' })
await changeEmailAddress({ email: 'ada@example.com' })
await changePassword({ currentPassword: 'old', newPassword: 'new-password', confirmNewPassword: 'new-password' })
Each call refreshes the user session afterwards; pending and error are refs.
const { items, links, current } = await useAccountNav()
useAccountNav() is what the layout, the heading and the overview read: items are the account navigation entries
(the account menu, or the enabled pages without the menu module), links the same as UNavigationMenu items
(localised, with active), and current the entry the route belongs to.
The module also auto-imports the components AccountHeader, AccountHeaderRight and AccountMenu.
AccountMenu
The signed-in user's dropdown: the user (with a role badge for anything but user), the account.dropdown menu
(Account, Admin, sign out), and Appearance (light, dark or system, setting useColorMode().preference). Items with
meta.roles are only listed for users holding one of those roles, the entry for the area you are in is left out, and
items with order 99 or more (sign out) come last.
| Prop | Type | Purpose |
|---|---|---|
block | boolean | A full-width row (avatar, name, chevrons) for a sidebar footer, as in the admin shell. Without it, an avatar button for a site header. |
collapsed | boolean | With block: only the avatar, for a collapsed sidebar. |
<AccountMenu />
<AccountMenu block :collapsed="collapsed" />
The colour mode lives here rather than in the header: set header.colorMode: false in the
base layer app config to drop the header toggle.
Overriding parts
The module's components have a lower priority than the app's, so a component with the same name in your app replaces
it. AccountHeaderRight is the block in the actions of the account heading and in the overview's summary card
(signed-in email by default); override it to show anything else, such as a member or Frigear ID badge:
<!-- app/components/Account/AccountHeaderRight.vue -->
<template>
<MyHeaderDetails />
</template>
Menu contributions
With Menu installed, each enabled page adds an item. The account menu is the account navigation
(and the overview's shortcuts); account.dropdown is AccountMenu. order sorts within the menu.
| Item | Menu | Order | Page |
|---|---|---|---|
account ("Account") | account.dropdown | -99 | pages.index |
account-overview ("Overview") | account | 0 | pages.index |
profile | account and account.dropdown | 10 | pages.profile |
security | account | 20 | pages.security |
api-tokens | account | 25 | pages.apiTokens |
documents | account | 30 | pages.documents |
Other modules slot into the same menu: Membership (Subscriptions) at 15, Tickets
(Events) at 40 and Applications (OAuth Server) at 50. Pick an order between
these for a new page. The source is registered for the account menu.
Page guard
With @frigear-nu/authorization installed, the module adds an authenticated: true route rule for /account and
/account/** (and each /<locale> prefix), so every page under it, including those other modules add (membership,
tickets, applications), is closed to guests. Set authorization.redirectTo to send guests to the login page instead of
a 401 error. Without the authorization module the pages are not guarded and the module logs a warning.
Routes
None. The module registers no server routes.
Translations
The module registers da and en locale files under the account.* namespace (key groups title, menu.*,
profile.*, documents.*, security.*; the full list is
src/runtime/i18n/lang/en.json; the layout and menu use nav.*, appearance.*, overview.* and accountMenu).
Without @nuxtjs/i18n the English defaults are used. Override strings in your app's
i18n/locales/<code>.json, see
overriding module strings:
{ "account": { "profile": { "title": "My details" } } }